Skip to content

Commit 1a99317

Browse files
authored
Update 80_brainstorm_phantom_elekta.py
add review comments from Dan
1 parent 7a62a34 commit 1a99317

1 file changed

Lines changed: 27 additions & 22 deletions

File tree

tutorials/inverse/80_brainstorm_phantom_elekta.py

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,11 @@
4141
# %%
4242
# Load and prepare the data
4343
# -------------------------
44-
44+
# TODO: convert to text from code comment
4545
# The data were collected with an Elekta Neuromag VectorView system
46-
# at 1000 Hz, low-pass filtered at 330 Hz and contains recordings
46+
# at 1000 Hz, low-pass filtered at 330 Hz and contain recordings
4747
# at three current amplitudes (20, 200, and 2000 nAm).
48+
4849
# Here we load the medium-amplitude condition.
4950
data_path = bst_phantom_elekta.data_path(verbose=True)
5051
raw_fname = data_path / "kojak_all_200nAm_pp_no_chpi_no_ms_raw.fif"
@@ -69,19 +70,22 @@
6970
epochs = mne.Epochs(
7071
raw, events, event_id, tmin, tmax, baseline=(None, bmax), preload=False
7172
)
72-
73-
# Here we plot the evoked response for the first clean dipole
74-
epochs["1"][1:-1].average().plot(time_unit="s")
73+
# We drop the first and last epoch as they contain artefacts
74+
epochs_clean = epochs[1:-1
75+
# We select the first simulated dipole for visualisation purposes
76+
epochs_firstdip = epochs_clean["1"]
77+
# Let's look at the evoked response for the first clean dipole
78+
# We can see that the phantom was set to produce 20 Hz sinusoidal bursts of current.
79+
# and the burst envelope repeats at approximately 3 Hz.
80+
epochs_firstdip.average().plot(time_unit="s")
81+
7582
# %%
76-
# In this data the phantom was set to produce 20 Hz sinusoidal bursts of current.
77-
# The burst envelope repeats at approximately 3 Hz.
78-
#
7983
# Determine peak activation using Global Field Power (GFP)
8084
# --------------------------------------------------------
8185

8286
# GFP is the standard deviation across sensors at each time
8387
# point, providing a reference-independent measure of signal strength.
84-
evoked_tmp = epochs["1"][1:-1].average()
88+
evoked_tmp = epochs_firstdip.average()
8589
gfp = np.std(evoked_tmp.data, axis=0)
8690
times = evoked_tmp.times
8791
# Restrict to first burst window
@@ -96,17 +100,18 @@
96100
# Here we crop the data at the peak amplitude and store the evoked data for each dipole.
97101
evokeds = []
98102
for ii in event_id:
99-
evoked = epochs[str(ii)][1:-1].average().crop(t_peak, t_peak)
103+
evoked = epochs_clean[str(ii)].average().crop(t_peak, t_peak)
100104
evoked = mne.EvokedArray(np.array(evoked.data), evoked.info, tmin=0.0)
101105
evokeds.append(evoked)
102106
# %%
103107
# Next, we need to compute the noise covariance to capture the sensor noise structure.
104108
# We use the baseline window to estimate covariance.
105109
# You can explore the covariance tutorial for details: :ref:`tut-compute-covariance`.
106110

107-
cov = mne.compute_covariance(epochs, tmax=bmax)
111+
cov = mne.compute_covariance(epochs_clean, tmax=bmax)
108112
del epochs # delete to save memory
109113
# %%
114+
# TODO: explain why this head model is used
110115
# We use a :ref:`sphere head geometry model <eeg_sphere_model>`
111116
# to fit our phantom head model.
112117
subjects_dir = data_path
@@ -127,6 +132,7 @@
127132
# Evaluate goodness of fit
128133
# ------------------------
129134

135+
# TODO: explain drop in GOF at regular intervals
130136
# The dipole object stores the goodness of fit (GOF) for each dipole.
131137
gof = [dip.gof[0] for dip in dip_all]
132138
colors = ["#E69F00" if val < 60 else "#0072B2" for val in gof]
@@ -136,14 +142,15 @@
136142
plt.show()
137143
#
138144
# %%
139-
# We can see that GOF varies between simulated dipoles from 50 % up to 95 %.
140-
#
141145
# Compare estimated and true dipoles
142146
# ----------------------------------
143147

148+
# The dipole fits closely match the true phantom data,
149+
# achieving sub-centimeter accuracy (mean position error 2.7mm).
150+
144151
# We get the true dipole positions from the phantoms
145152
actual_pos, actual_ori = mne.dipole.get_phantom_dipoles()
146-
actual_amp = 200.0 # nAm
153+
actual_amp = 100.0 # nAm
147154

148155
# estimated dipoles
149156
dip_pos = [dip.pos[0] for dip in dip_all]
@@ -177,12 +184,13 @@
177184
ax3.set_xlabel("Dipole index")
178185
ax3.set_ylabel("Amplitude error (nAm)")
179186
# %%
180-
# The dipole fits closely match the true phantom data,
181-
# achieving sub-centimeter accuracy (mean position error 2.7mm).
182-
#
183-
# Visualise estimated and true dipole fits
184-
# ----------------------------------------
187+
# Visualise estimated and true dipole locations
188+
# ---------------------------------------------
185189

190+
191+
# We can see that the dipoles overlap, have approximately the same magnitude
192+
# and point in the same direction.
193+
186194
actual_amp = np.ones(len(dip)) # fake amp, needed to create Dipole instance
187195
actual_gof = np.ones(len(dip)) # fake goodness-of-fit (GOF)
188196
# setup dipole objects for true and estimated dipoles
@@ -214,9 +222,6 @@
214222
dipoles=dip_estimated, mode="arrow", subject=subject, color=(0.2, 1.0, 0.5), fig=fig
215223
)
216224
mne.viz.set_3d_view(figure=fig, azimuth=70, elevation=80, distance=0.5)
217-
# %%
218-
# We can see that the dipoles overlap, have approximately the same magnitude
219-
# and point in the same direction.
220225

221226
# %%
222227
# References

0 commit comments

Comments
 (0)