Skip to content

Commit cc377da

Browse files
authored
ENH: Enable sensor-specific OPM coregistration in mne coreg (#11405)
1 parent 7b6b795 commit cc377da

File tree

30 files changed

+690
-96
lines changed

30 files changed

+690
-96
lines changed

.circleci/config.yml

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -223,13 +223,16 @@ jobs:
223223
- data-cache-fsaverage
224224
- restore_cache:
225225
keys:
226-
- data-cache-bst-phantom-ctf
226+
- data-cache-bst-raw
227227
- restore_cache:
228228
keys:
229-
- data-cache-bst-raw
229+
- data-cache-bst-phantom-ctf
230230
- restore_cache:
231231
keys:
232232
- data-cache-bst-phantom-elekta
233+
- restore_cache:
234+
keys:
235+
- data-cache-bst-phantom-kernel
233236
- restore_cache:
234237
keys:
235238
- data-cache-bst-auditory
@@ -368,18 +371,22 @@ jobs:
368371
key: data-cache-fsaverage
369372
paths:
370373
- ~/mne_data/MNE-fsaverage-data # (762 M)
371-
- save_cache:
372-
key: data-cache-bst-phantom-ctf
373-
paths:
374-
- ~/mne_data/MNE-brainstorm-data/bst_phantom_ctf # (177 M)
375374
- save_cache:
376375
key: data-cache-bst-raw
377376
paths:
378377
- ~/mne_data/MNE-brainstorm-data/bst_raw # (830 M)
378+
- save_cache:
379+
key: data-cache-bst-phantom-ctf
380+
paths:
381+
- ~/mne_data/MNE-brainstorm-data/bst_phantom_ctf # (177 M)
379382
- save_cache:
380383
key: data-cache-bst-phantom-elekta
381384
paths:
382385
- ~/mne_data/MNE-brainstorm-data/bst_phantom_elekta # (1.4 G)
386+
- save_cache:
387+
key: data-cache-bst-phantom-kernel
388+
paths:
389+
- ~/mne_data/MNE-phantom-kernel-data # (362 M)
383390
- save_cache:
384391
key: data-cache-bst-auditory
385392
paths:

doc/api/datasets.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Datasets
4141
ucl_opm_auditory.data_path
4242
visual_92_categories.data_path
4343
phantom_4dbti.data_path
44+
phantom_kernel.data_path
4445
refmeg_noise.data_path
4546
ssvep.data_path
4647
erp_core.data_path

doc/documentation/datasets.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,19 @@ the MEG center in La Timone hospital in Marseille.
293293

294294
* :ref:`tut-phantom-4Dbti`
295295

296+
Kernel OPM phantom dataset
297+
==========================
298+
:func:`mne.datasets.phantom_kernel.data_path`.
299+
300+
This dataset was obtained with a Neuromag phantom in a Kernel Flux (720-sensor)
301+
system at ILABS at the University of Washington. Only 7 out of 42 possible modules
302+
were active for testing purposes, yielding 121 channels of data with limited coverage
303+
(mostly occipital and parietal).
304+
305+
.. topic:: Examples
306+
307+
* :ref:`ex-kernel-opm-phantom`
308+
296309
OPM
297310
===
298311
:func:`mne.datasets.opm.data_path`
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""
2+
.. _ex-kernel-opm-phantom:
3+
4+
Kernel OPM phantom data
5+
=======================
6+
7+
In this dataset, a Neuromag phantom was placed inside the Kernel OPM helmet and
8+
stimulated with 7 modules active (121 channels). Here we show some example traces.
9+
"""
10+
11+
import numpy as np
12+
13+
import mne
14+
15+
data_path = mne.datasets.phantom_kernel.data_path()
16+
fname = data_path / "phantom_32_100nam_raw.fif"
17+
raw = mne.io.read_raw_fif(fname).load_data()
18+
events = mne.find_events(raw, stim_channel="STI101")
19+
20+
# Bads identified by inspecting averages
21+
raw.info["bads"] = [
22+
"RC2.bx.ave",
23+
"LC3.bx.ave",
24+
"RC2.by.7",
25+
"RC2.bz.7",
26+
"RC2.bx.4",
27+
"RC2.by.4",
28+
"LC3.bx.5",
29+
]
30+
# Drop the module-average channels
31+
raw.drop_channels([ch_name for ch_name in raw.ch_names if ".ave" in ch_name])
32+
# Add field correction projectors
33+
raw.add_proj(mne.preprocessing.compute_proj_hfc(raw.info, order=2))
34+
raw.pick("meg", exclude="bads")
35+
raw.filter(0.5, 40)
36+
epochs = mne.Epochs(
37+
raw,
38+
events,
39+
tmin=-0.1,
40+
tmax=0.25,
41+
decim=5,
42+
preload=True,
43+
baseline=(None, 0),
44+
)
45+
evoked = epochs["17"].average() # a high-SNR dipole for these data
46+
fig = evoked.plot()
47+
t_peak = 0.016 # based on visual inspection of evoked
48+
fig.axes[0].axvline(t_peak, color="k", ls=":", lw=3, zorder=2)
49+
50+
# %%
51+
# The data covariance has an interesting structure because of densely packed sensors:
52+
cov = mne.compute_covariance(epochs, tmax=-0.01)
53+
mne.viz.plot_cov(cov, raw.info)
54+
55+
# %%
56+
# So let's be careful and compute rank ahead of time and regularize:
57+
58+
rank = mne.compute_rank(epochs, tol=1e-3, tol_kind="relative")
59+
cov = mne.compute_covariance(epochs, tmax=-0.01, rank=rank, method="shrunk")
60+
mne.viz.plot_cov(cov, raw.info)
61+
62+
# %%
63+
# Look at our alignment:
64+
65+
sphere = mne.make_sphere_model(r0=(0.0, 0.0, 0.0), head_radius=0.08)
66+
trans = mne.transforms.Transform("head", "mri", np.eye(4))
67+
align_kwargs = dict(
68+
trans=trans,
69+
bem=sphere,
70+
surfaces={"outer_skin": 0.2},
71+
show_axes=True,
72+
)
73+
mne.viz.plot_alignment(
74+
raw.info,
75+
coord_frame="meg",
76+
meg=dict(sensors=1.0, helmet=0.05),
77+
**align_kwargs,
78+
)
79+
80+
# %%
81+
# Let's do dipole fits, which are not great because the dev_head_t is approximate and
82+
# the sensor coverage is sparse:
83+
84+
data = list()
85+
for ii in range(1, 33):
86+
evoked = epochs[str(ii)][1:-1].average().crop(t_peak, t_peak)
87+
data.append(evoked.data[:, 0])
88+
evoked = mne.EvokedArray(np.array(data).T, evoked.info, tmin=0.0)
89+
del epochs
90+
dip, residual = mne.fit_dipole(evoked, cov, sphere, n_jobs=None)
91+
actual_pos, actual_ori = mne.dipole.get_phantom_dipoles()
92+
actual_amp = np.ones(len(dip)) # fake amp, needed to create Dipole instance
93+
actual_gof = np.ones(len(dip)) # fake GOF, needed to create Dipole instance
94+
dip_true = mne.Dipole(dip.times, actual_pos, actual_amp, actual_ori, actual_gof)
95+
96+
fig = mne.viz.plot_alignment(
97+
evoked.info, coord_frame="head", meg="sensors", **align_kwargs
98+
)
99+
mne.viz.plot_dipole_locations(
100+
dipoles=dip_true, mode="arrow", color=(0.0, 0.0, 0.0), fig=fig
101+
)
102+
mne.viz.plot_dipole_locations(dipoles=dip, mode="arrow", color=(0.2, 1.0, 0.5), fig=fig)
103+
mne.viz.set_3d_view(figure=fig, azimuth=30, elevation=70, distance=0.4)

mne/_fiff/_digitization.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ def _get_data_as_dict_from_dig(dig, exclude_ref_channel=True):
293293
# Split up the dig points by category
294294
hsp, hpi, elp = list(), list(), list()
295295
fids, dig_ch_pos_location = dict(), list()
296+
dig = [] if dig is None else dig
296297

297298
for d in dig:
298299
if d["kind"] == FIFF.FIFFV_POINT_CARDINAL:
@@ -307,6 +308,8 @@ def _get_data_as_dict_from_dig(dig, exclude_ref_channel=True):
307308
dig_ch_pos_location.append(d["r"])
308309

309310
dig_coord_frames = set([d["coord_frame"] for d in dig])
311+
if len(dig_coord_frames) == 0:
312+
dig_coord_frames = set([FIFF.FIFFV_COORD_HEAD])
310313
if len(dig_coord_frames) != 1:
311314
raise RuntimeError(
312315
"Only single coordinate frame in dig is supported, "

mne/bem.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1751,13 +1751,15 @@ def _add_gamma_multipliers(bem):
17511751
FIFF.FIFFV_BEM_SURF_ID_SKULL: "outer skull",
17521752
FIFF.FIFFV_BEM_SURF_ID_HEAD: "outer skin ",
17531753
FIFF.FIFFV_BEM_SURF_ID_UNKNOWN: "unknown ",
1754+
FIFF.FIFFV_MNE_SURF_MEG_HELMET: "MEG helmet ",
17541755
}
17551756
_sm_surf_name = {
17561757
FIFF.FIFFV_BEM_SURF_ID_BRAIN: "brain",
17571758
FIFF.FIFFV_BEM_SURF_ID_CSF: "csf",
17581759
FIFF.FIFFV_BEM_SURF_ID_SKULL: "outer skull",
17591760
FIFF.FIFFV_BEM_SURF_ID_HEAD: "outer skin ",
17601761
FIFF.FIFFV_BEM_SURF_ID_UNKNOWN: "unknown ",
1762+
FIFF.FIFFV_MNE_SURF_MEG_HELMET: "helmet",
17611763
}
17621764

17631765

@@ -1850,7 +1852,8 @@ def _write_bem_surfaces_block(fid, surfs):
18501852
"""Write bem surfaces to open file handle."""
18511853
for surf in surfs:
18521854
start_block(fid, FIFF.FIFFB_BEM_SURF)
1853-
write_float(fid, FIFF.FIFF_BEM_SIGMA, surf["sigma"])
1855+
if "sigma" in surf:
1856+
write_float(fid, FIFF.FIFF_BEM_SIGMA, surf["sigma"])
18541857
write_int(fid, FIFF.FIFF_BEM_SURF_ID, surf["id"])
18551858
write_int(fid, FIFF.FIFF_MNE_COORD_FRAME, surf["coord_frame"])
18561859
write_int(fid, FIFF.FIFF_BEM_SURF_NNODE, surf["np"])

mne/channels/channels.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ def _get_meg_system(info):
104104
system = "ARTEMIS123"
105105
have_helmet = False
106106
break
107+
elif coil_type == FIFF.FIFFV_COIL_KERNEL_OPM_MAG_GEN1:
108+
system = "Kernel_Flux"
109+
have_helmet = True
110+
break
107111
else:
108112
system = "unknown"
109113
have_helmet = False

mne/coreg.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1550,7 +1550,9 @@ def _setup_bem(self):
15501550
low_res_path = _find_head_bem(self._subject, self._subjects_dir, high_res=False)
15511551
if high_res_path is None and low_res_path is None:
15521552
raise RuntimeError(
1553-
"No standard head model was " f"found for subject {self._subject}"
1553+
"No standard head model was "
1554+
f"found for subject {self._subject} in "
1555+
f"{self._subjects_dir}"
15541556
)
15551557
if high_res_path is not None:
15561558
self._bem_high_res = _read_surface(
@@ -1987,9 +1989,9 @@ def fit_fiducials(
19871989
return self
19881990

19891991
def _setup_icp(self, n_scale_params):
1990-
head_pts = list()
1991-
mri_pts = list()
1992-
weights = list()
1992+
head_pts = [np.zeros((0, 3))]
1993+
mri_pts = [np.zeros((0, 3))]
1994+
weights = [np.zeros(0)]
19931995
if self._has_dig_data and self._hsp_weight > 0: # should be true
19941996
head_pts.append(self._filtered_extra_points)
19951997
mri_pts.append(
481 KB
Binary file not shown.

0 commit comments

Comments
 (0)