|
22 | 22 | # sphinx_gallery_thumbnail_number = 9 |
23 | 23 |
|
24 | 24 | # Authors: Eric Larson <larson.eric.d@gmail.com> |
| 25 | +# Carina Forster <carinaforster0611@gmail.com> |
25 | 26 | # |
26 | 27 | # License: BSD-3-Clause |
27 | 28 | # Copyright the MNE-Python contributors. |
|
39 | 40 | from mne.io import read_raw_fif |
40 | 41 |
|
41 | 42 | print(__doc__) |
42 | | - |
43 | 43 | # %% |
| 44 | +# Load and prepare the data |
| 45 | +# ------------------------------- |
44 | 46 | # The data were collected with an Elekta Neuromag VectorView system |
45 | 47 | # at 1000 Hz and low-pass filtered at 330 Hz. |
46 | 48 | # The dataset has recordings at 3 different current amplitudes (20, 200, and 2000 nAm), |
|
49 | 51 | raw_fname = data_path / "kojak_all_200nAm_pp_no_chpi_no_ms_raw.fif" |
50 | 52 | raw = read_raw_fif(raw_fname) |
51 | 53 | raw.info["bads"] = ["MEG1933", "MEG2421"] # known bad channels |
52 | | -# |
53 | | -# Here we store the events for one stimulus channel. |
54 | 54 | events = find_events(raw, "STI201") |
55 | 55 | # The first 32 events are the dipole phantoms recorded. |
56 | 56 | # The remaining event IDs are not relevant for this tutorial. |
57 | | - |
58 | | -# Next, we epoch the data for each dipole event |
| 57 | +# %% |
| 58 | +# Here we epoch the data around dipole events |
59 | 59 | # and baseline correct the epochs from -100 ms to -0.05 ms before stimulus onset. |
60 | 60 | bmax = -0.05 |
61 | 61 | tmin, tmax = -0.1, 0.8 |
|
66 | 66 | # %% |
67 | 67 | # Dipole fitting on phantom data |
68 | 68 | # ------------------------------- |
69 | | -# We use the epoched data to fit dipoles, |
70 | | -# then compare them to the known dipole locations built into the phantom. |
| 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 | 71 |
|
72 | 72 | # We use the first dipole event to plot the evoked signal |
73 | | -# and skip first and last epoch as they are corrupted. |
| 73 | +# and skip the first and the last epoch as they are corrupted. |
| 74 | + |
74 | 75 | epochs["1"][1:-1].average().plot(time_unit="s") |
75 | | -evoked_tmp = epochs["1"][1:-1].average() |
76 | | -# We averaged over 18 simulated events for the first dipole. |
| 76 | + |
| 77 | +# We average over 18 simulated events for the first dipole. |
77 | 78 | # In this data the phantom was set to produce 20 Hz sinusoidal bursts of current. |
78 | 79 | # You can see that the burst envelope repeats at approximately 3 Hz. |
79 | | - |
| 80 | +# %% |
80 | 81 | # First, we need to determine the timepoint of the peak amplitude. |
81 | | -# To find this timepoint, we compute Global Field Power (GFP) |
| 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. |
| 85 | +evoked_tmp = epochs["1"][1:-1].average() |
82 | 86 | gfp = np.std(evoked_tmp.data, axis=0) |
83 | 87 | times = evoked_tmp.times |
84 | 88 |
|
85 | | -# We wamt the peak for the first bursting event |
| 89 | +# We restrict the time window to find the peak |
| 90 | +# for the first bursting event. |
86 | 91 | time_mask = (times > 0) & (times <= 0.1) |
87 | | - |
88 | | -# Find peaks in that window |
89 | 92 | peaks, _ = find_peaks(gfp[time_mask]) |
90 | | - |
91 | 93 | # Convert to original indices |
92 | 94 | peak_indices = np.where(time_mask)[0][peaks] |
93 | | - |
94 | 95 | # Select the strongest peak (max GFP) |
95 | 96 | strongest_peak_idx = peak_indices[np.argmax(gfp[peak_indices])] |
96 | 97 | t_peak = times[strongest_peak_idx] |
97 | 98 |
|
98 | 99 | print(f"Strongest peak at {t_peak * 1000:.1f} ms") |
99 | | - |
| 100 | +# %% |
100 | 101 | # Here we store the evoked data for each dipole at the peak amplitude. |
101 | 102 | evokeds = [] |
102 | 103 | for ii in event_id: |
103 | 104 | evoked = epochs[str(ii)][1:-1].average().crop(t_peak, t_peak) |
104 | 105 | evoked = mne.EvokedArray(np.array(evoked.data), evoked.info, tmin=0.0) |
105 | 106 | evokeds.append(evoked) |
106 | | - |
| 107 | +# %% |
107 | 108 | # Next, we need to compute the noise covariance to capture the sensor noise structure. |
108 | 109 | # We use the baseline window to estimate covariance. |
109 | 110 | # You can explore the covariance tutorial for details :ref:`tut-compute-covariance`. |
| 111 | + |
110 | 112 | cov = mne.compute_covariance(epochs, tmax=bmax) |
111 | 113 |
|
112 | 114 | del epochs # delete to save memory |
113 | | - |
| 115 | +# %% |
114 | 116 | # We use a :ref:`sphere head geometry model <eeg_sphere_model>` |
115 | 117 | # to fit our phantom head model. |
116 | 118 | subjects_dir = data_path |
117 | 119 | fetch_phantom("otaniemi", subjects_dir=subjects_dir) |
118 | 120 | sphere = mne.make_sphere_model(r0=(0.0, 0.0, 0.0), head_radius=0.08) |
119 | 121 |
|
| 122 | +# %% |
| 123 | +# We finally fit all 32 phantom dipoles and store them |
| 124 | +# as well as the residuals in a list. |
| 125 | + |
120 | 126 | dip_all, residuals_all = [], [] |
121 | | -# Let's finally fit all 32 phantom dipoles. |
| 127 | + |
122 | 128 | for evoked in evokeds: |
123 | 129 | dip, residual = fit_dipole(evoked, cov, sphere, n_jobs=1) |
124 | 130 | dip_all.append(dip) |
|
136 | 142 | plt.xlabel("Phantom dipole estimation") |
137 | 143 | plt.ylabel("Goodness of fit (%)") |
138 | 144 | plt.show() |
| 145 | +# %% |
139 | 146 | # We can see that GOF varies between 50 and more than 95 % |
140 | 147 | # variance explained for dipoles. |
141 | 148 | # %% |
142 | | - |
| 149 | +# Estimated vs true dipole locations |
| 150 | +# ------------------------------- |
143 | 151 | # Finally, we compare the estimated to the true dipole locations. |
144 | 152 | actual_pos, actual_ori = mne.dipole.get_phantom_dipoles() |
145 | 153 | actual_amp = 200.0 # nAm |
146 | | -# dipole estimations |
| 154 | +# estimated dipoles |
147 | 155 | dip_pos = [dip.pos[0] for dip in dip_all] |
148 | 156 | dip_ori = [dip.ori[0] for dip in dip_all] |
149 | 157 | dip_amplitude = [dip.amplitude[0] for dip in dip_all] |
|
168 | 176 | ax2.set_xlabel("Dipole index") |
169 | 177 | ax2.set_ylabel("Angle error (°)") |
170 | 178 |
|
171 | | -# Finally we compare amplitudes by subtracting estimated from true amplitude. |
| 179 | +# Here we compare amplitudes by subtracting estimated from true amplitude. |
172 | 180 | amps = actual_amp - np.array(dip_amplitude) / 1e-9 |
173 | 181 | print(f"mean(abs amplitude error) = {np.mean(np.abs(amps)):0.1f} nAm") |
174 | 182 | ax3.bar(event_id, amps) |
175 | 183 | ax3.set_xlabel("Dipole index") |
176 | 184 | ax3.set_ylabel("Amplitude error (nAm)") |
177 | 185 | # %% |
178 | | -# TODO: fix dipole vis and format the docs |
179 | 186 | # The dipole fits closely match the true phantom data, |
180 | 187 | # achieving sub-centimeter accuracy (mean position error 2.7mm). |
181 | 188 | # |
|
203 | 210 | fig = mne.viz.plot_dipole_locations( |
204 | 211 | dipoles=dip_true, mode="arrow", subject=subject, color=(0.0, 0.0, 0.0), fig=fig |
205 | 212 | ) |
206 | | -# Plot the position and the orientation of the estimated dipole in green |
207 | | -fig = mne.viz.plot_dipole_locations( |
208 | | - dipoles=dip, mode="arrow", subject=subject, color=(0.2, 1.0, 0.5), fig=fig |
209 | | -) |
| 213 | +for dip in dip_all: |
| 214 | + # Plot the position and the orientation of the estimated dipole in green |
| 215 | + fig = mne.viz.plot_dipole_locations( |
| 216 | + dipoles=dip, mode="arrow", subject=subject, color=(0.2, 1.0, 0.5), fig=fig |
| 217 | + ) |
210 | 218 | mne.viz.set_3d_view(figure=fig, azimuth=70, elevation=80, distance=0.5) |
211 | 219 | # %% |
212 | 220 | # We can see that the dipoles overlap, have approximately the same magnitude |
|
0 commit comments