|
41 | 41 | # %% |
42 | 42 | # Load and prepare the data |
43 | 43 | # ------------------------- |
44 | | -# TODO: convert to text from code comment |
45 | 44 | # The data were collected with an Elekta Neuromag VectorView system |
46 | 45 | # at 1000 Hz, low-pass filtered at 330 Hz and contain recordings |
47 | 46 | # at three current amplitudes (20, 200, and 2000 nAm). |
48 | | - |
49 | 47 | # Here we load the medium-amplitude condition. |
| 48 | + |
| 49 | +# Load the data |
50 | 50 | data_path = bst_phantom_elekta.data_path(verbose=True) |
51 | 51 | raw_fname = data_path / "kojak_all_200nAm_pp_no_chpi_no_ms_raw.fif" |
52 | 52 | raw = read_raw_fif(raw_fname) |
53 | 53 |
|
54 | 54 | # Mark known bad channels |
55 | 55 | raw.info["bads"] = ["MEG1933", "MEG2421"] |
56 | 56 |
|
57 | | -events = find_events(raw, "STI201") |
58 | 57 | # The first 32 events correspond to dipole activations. |
| 58 | +events = find_events(raw, "STI201") |
| 59 | + |
59 | 60 |
|
60 | 61 | # %% |
61 | 62 | # Epoch the data and plot evokeds |
62 | 63 | # ------------------------------- |
63 | | -# |
64 | 64 | # We epoch and baseline correct the data around the dipole events. |
65 | 65 |
|
| 66 | +# Epoch the data |
66 | 67 | bmax = -0.05 |
67 | 68 | tmin, tmax = -0.1, 0.8 |
68 | 69 | event_id = list(range(1, 33)) |
69 | | - |
70 | 70 | epochs = mne.Epochs( |
71 | 71 | raw, events, event_id, tmin, tmax, baseline=(None, bmax), preload=False |
72 | 72 | ) |
73 | | -# We drop the first and last epoch as they contain artefacts |
74 | | -epochs_clean = epochs[1:-1 |
| 73 | + |
| 74 | +# We drop the first and last event, it can contains dipole-switching artifacts |
| 75 | +epochs_clean = epochs[1:-1] |
| 76 | + |
75 | 77 | # We select the first simulated dipole for visualisation purposes |
76 | 78 | epochs_firstdip = epochs_clean["1"] |
| 79 | + |
| 80 | +# %% |
77 | 81 | # Let's look at the evoked response for the first clean dipole |
78 | 82 | # We can see that the phantom was set to produce 20 Hz sinusoidal bursts of current. |
79 | 83 | # and the burst envelope repeats at approximately 3 Hz. |
| 84 | + |
80 | 85 | epochs_firstdip.average().plot(time_unit="s") |
81 | | - |
| 86 | + |
82 | 87 | # %% |
83 | 88 | # Determine peak activation using Global Field Power (GFP) |
84 | 89 | # -------------------------------------------------------- |
85 | | - |
86 | 90 | # GFP is the standard deviation across sensors at each time |
87 | 91 | # point, providing a reference-independent measure of signal strength. |
| 92 | + |
| 93 | +# Get the evoked signal of the first dipole |
88 | 94 | evoked_tmp = epochs_firstdip.average() |
| 95 | +# Calculate GFP |
89 | 96 | gfp = np.std(evoked_tmp.data, axis=0) |
90 | | -times = evoked_tmp.times |
| 97 | + |
91 | 98 | # Restrict to first burst window |
92 | | -time_mask = (times > 0) & (times <= 0.1) |
| 99 | +times = evoked_tmp.times |
| 100 | +time_mask = (times > 0) & (times <= 0.05) |
| 101 | + |
| 102 | +# Find the peak GFP indices |
93 | 103 | peaks, _ = find_peaks(gfp[time_mask]) |
94 | 104 | peak_indices = np.where(time_mask)[0][peaks] |
| 105 | + |
95 | 106 | # Select the strongest peak |
96 | 107 | strongest_peak_idx = peak_indices[np.argmax(gfp[peak_indices])] |
97 | 108 | t_peak = times[strongest_peak_idx] |
98 | 109 | print(f"Strongest peak at {t_peak * 1000:.1f} ms") |
99 | 110 | # %% |
100 | | -# Here we crop the data at the peak amplitude and store the evoked data for each dipole. |
| 111 | +# Here we select the peak amplitude timepoint and store the evoked data for each dipole. |
| 112 | + |
101 | 113 | evokeds = [] |
102 | 114 | for ii in event_id: |
103 | 115 | evoked = epochs_clean[str(ii)].average().crop(t_peak, t_peak) |
|
111 | 123 | cov = mne.compute_covariance(epochs_clean, tmax=bmax) |
112 | 124 | del epochs # delete to save memory |
113 | 125 | # %% |
114 | | -# TODO: explain why this head model is used |
| 126 | +# %% |
115 | 127 | # We use a :ref:`sphere head geometry model <eeg_sphere_model>` |
116 | | -# to fit our phantom head model. |
| 128 | +# because the Elekta phantom is designed to approximate a spherical |
| 129 | +# conductor with known dipole locations. |
| 130 | + |
117 | 131 | subjects_dir = data_path |
118 | 132 | fetch_phantom("otaniemi", subjects_dir=subjects_dir) |
119 | 133 | sphere = mne.make_sphere_model(r0=(0.0, 0.0, 0.0), head_radius=0.08) |
120 | 134 |
|
121 | 135 | # %% |
122 | 136 | # Dipole fitting |
123 | 137 | # -------------- |
124 | | - |
125 | 138 | # Finally, we fit dipoles for each phantom and store them in a list. |
| 139 | + |
126 | 140 | dip_all = [] |
127 | 141 |
|
128 | 142 | for evoked in evokeds: |
|
131 | 145 | # %% |
132 | 146 | # Evaluate goodness of fit |
133 | 147 | # ------------------------ |
134 | | - |
135 | | -# TODO: explain drop in GOF at regular intervals |
136 | 148 | # The dipole object stores the goodness of fit (GOF) for each dipole. |
| 149 | +# Some dipoles have lower GOF because... |
137 | 150 | gof = [dip.gof[0] for dip in dip_all] |
138 | 151 | colors = ["#E69F00" if val < 60 else "#0072B2" for val in gof] |
139 | 152 | plt.bar(event_id, gof, color=colors) |
140 | 153 | plt.xlabel("Phantom dipole estimation") |
141 | 154 | plt.ylabel("Goodness of fit (%)") |
142 | 155 | plt.show() |
143 | | -# |
| 156 | + |
| 157 | +# %% |
| 158 | +# Dipoles with low goodness of fit |
| 159 | +# -------------------------------- |
| 160 | +# Why do some dipoles have a low (<60) GOF? |
| 161 | +# Here we plot the dipole locations of the dipoles with low GOF. |
| 162 | +# The dipoles with low GOF are deep in the brain which might explain |
| 163 | +# the low GOF. |
| 164 | + |
| 165 | +# Get indices of low GOF dipoles |
| 166 | +low_idx = [i for i, g in enumerate(gof) if g < 60] |
| 167 | +low_event_ids = [event_id[i] for i in low_idx] |
| 168 | + |
| 169 | +print("Low GOF dipoles:", low_event_ids) |
| 170 | + |
| 171 | +# Let's plot the locations of the dipoles with low GOF. |
| 172 | +low_dips = [dip_all[i] for i in low_idx] |
| 173 | + |
| 174 | +subject = "phantom_otaniemi" |
| 175 | +trans = mne.transforms.Transform("head", "mri", np.eye(4)) |
| 176 | + |
| 177 | +fig = mne.viz.plot_alignment( |
| 178 | + evoked.info, |
| 179 | + trans, |
| 180 | + subject, |
| 181 | + bem=sphere, |
| 182 | + surfaces={"head-dense": 0.2}, |
| 183 | + coord_frame="head", |
| 184 | + meg="helmet", |
| 185 | + show_axes=True, |
| 186 | + subjects_dir=subjects_dir, |
| 187 | +) |
| 188 | + |
| 189 | +# Plot the position and the orientation of the dipoles with low GOF |
| 190 | +fig = mne.viz.plot_dipole_locations( |
| 191 | + dipoles=low_dips, mode="arrow", subject=subject, color=(1.0, 0.2, 0.2), fig=fig |
| 192 | +) |
144 | 193 | # %% |
145 | 194 | # Compare estimated and true dipoles |
146 | 195 | # ---------------------------------- |
147 | | - |
148 | 196 | # The dipole fits closely match the true phantom data, |
149 | 197 | # achieving sub-centimeter accuracy (mean position error 2.7mm). |
150 | 198 |
|
151 | 199 | # We get the true dipole positions from the phantoms |
152 | 200 | actual_pos, actual_ori = mne.dipole.get_phantom_dipoles() |
153 | 201 | actual_amp = 100.0 # nAm |
154 | 202 |
|
155 | | -# estimated dipoles |
| 203 | +# Here we store the estimated dipoles |
156 | 204 | dip_pos = [dip.pos[0] for dip in dip_all] |
157 | 205 | dip_ori = [dip.ori[0] for dip in dip_all] |
158 | 206 | dip_amplitude = [dip.amplitude[0] for dip in dip_all] |
|
186 | 234 | # %% |
187 | 235 | # Visualise estimated and true dipole locations |
188 | 236 | # --------------------------------------------- |
189 | | - |
190 | | - |
191 | 237 | # We can see that the dipoles overlap, have approximately the same magnitude |
192 | 238 | # and point in the same direction. |
193 | | - |
| 239 | + |
194 | 240 | actual_amp = np.ones(len(dip)) # fake amp, needed to create Dipole instance |
195 | 241 | actual_gof = np.ones(len(dip)) # fake goodness-of-fit (GOF) |
196 | 242 | # setup dipole objects for true and estimated dipoles |
197 | 243 | dip_true = mne.Dipole(dip.times, actual_pos, actual_amp, actual_ori, actual_gof) |
198 | 244 | dip_estimated = mne.Dipole(dip.times, dip_pos, dip_amplitude, dip_ori, actual_gof) |
199 | 245 |
|
200 | | -subject = "phantom_otaniemi" |
201 | | -trans = mne.transforms.Transform("head", "mri", np.eye(4)) |
202 | | - |
203 | 246 | fig = mne.viz.plot_alignment( |
204 | 247 | evoked.info, |
205 | 248 | trans, |
|
0 commit comments