Description
Description of the problem
When we estimated the time-varying head position with the MEG data that we had collected (306-channel Vectorview, Elekta Neuromag), we found that sometimes the estimated head position was unstable consistently “teleporting” between two locations over time, which couldn’t be thought of as actual human movements.
5 HPI coils were installed during the MEG acquisition (261, 278, 294, 311, and 328 Hz) and the head position was computed by applying three chpi functions sequentially to these 5 HPI coils (i.e., mne.chpi.compute_chpi_amplitudes
-> mne.chpi.compute_chpi_locs
-> mne.chpi.compute_head_pos
).
I tried to figure out with Dr. Burkhard Maess (@bmaess, at MPI CBS) in which part this weird pattern was produced.
Adjusting the size of a window and a step didn’t help to solve the problem, and we observed that this problem could happen even when the estimated cHPI locations (results of mne.chpi.compute_chpi_locs) were actually smooth and showed good gofs (goodness of fit).
Steps to reproduce
import os
import mne
import matplotlib.pyplot as plt
raw_file = fpath + os.sep + 'te01a1.fif' # 'fpath' is the path where the sample file is stored
## 1) compute head positions
raw = mne.io.read_raw_fif(raw_file)
# compute cHPI amplitudes
chpi_amps = mne.chpi.compute_chpi_amplitudes(raw, t_step_min=0.25, t_window=0.5)
# compute cHPI locations
chpi_locs = mne.chpi.compute_chpi_locs(raw.info, chpi_amps)
# compute head position
head_pos = mne.chpi.compute_head_pos(raw.info, chpi_locs)
mne.viz.plot_head_positions(head_pos)
## 2) plot the estimated cHPI locations (i.e., chpi_locs)
t = chpi_locs['times']
coil2plot = 1 # from 1 to 5
locs = chpi_locs['rrs'][:,coil2plot-1,:]
moments = chpi_locs['moments'][:,coil2plot-1,:]
fig, ax = plt.subplots(3,2)
fig.suptitle(f'Locs and Moments of the HPI coil %d' % coil2plot)
ax[0,0].set_title('Locations')
ax[0,0].plot(t, locs[:,0])
ax[0,0].set_ylabel('x')
ax[1,0].plot(t, locs[:,1])
ax[1,0].set_ylabel('y')
ax[2,0].plot(t, locs[:,2])
ax[2,0].set_ylabel('z')
ax[2,0].set_xlabel('time [s]')
ax[0,1].set_title('Moments')
ax[0,1].plot(t, moments[:,0])
ax[0,1].set_ylabel('x')
ax[1,1].plot(t, moments[:,1])
ax[1,1].set_ylabel('y')
ax[2,1].plot(t, moments[:,2])
ax[2,1].set_ylabel('z')
ax[2,1].set_xlabel('time [s]')
# The figure title should not overlap with the subplots.
fig.tight_layout(rect=[0, 0.03, 1, 0.95])
Link to data
Expected results
This is the figure that we got after we had debugged mne.chpi.compute_head_pos
to fix the problem.
We found that inside of mne.chpi.compute_head_pos
, there is a function mne.chpi._fit_chpi_quat_subset
that chooses maximum 3 HPI coils based on the gof (goodness of fit) to estimate the head position at a given time point.
Lines 939 to 949 in 0fbd8fc
This sometimes caused the different members of the HPI coils to be selected for the different time points. It could make the basis for estimating the head position change over time, and thus the estimated head position could be unstable. In fact, we observed that the sudden jumps or teleportation in the estimated head position coincide with the changes in the members of the HPI coils in use.
## a code to replicate the above figure
import os
import numpy as np
import mne
import matplotlib.pyplot as plt
raw_file = fpath + os.sep + 'te01a1.fif' # 'fpath' is the path where the sample file is stored
# compute head positions
raw = mne.io.read_raw_fif(raw_file)
chpi_amps = mne.chpi.compute_chpi_amplitudes(raw, t_step_min=0.25, t_window=0.5) # compute cHPI amplitudes
chpi_locs = mne.chpi.compute_chpi_locs(raw.info, chpi_amps) # compute cHPI locations
head_pos = mne.chpi.compute_head_pos(raw.info, chpi_locs) # compute head position
# find out the coil indices used for each time point
times = chpi_locs['times']
coil_idx_mtx = np.empty([3, len(times)])
time_idx = 0
hpi_dig_head_rrs = mne.chpi._get_hpi_initial_fit(raw.info, adjust=None, verbose='error')
for fit_time, this_coil_dev_rrs, g_coils in zip(
*(chpi_locs[key] for key in ('times', 'rrs', 'gofs'))):
use_idx = np.where(g_coils >= 0.98)[0]
_, _, use_idx = mne.chpi._fit_chpi_quat_subset(this_coil_dev_rrs, hpi_dig_head_rrs, use_idx)
coil_idx_mtx[:,count] = use_idx[:]
time_idx += 1
# plot the sum of coil indices with head position
t = head_pos[:,0]
coils = np.sum(coil_idx_mtx, axis=0)
fig, ax = plt.subplots(3,1)
fig.suptitle(f'head position with the selected coils’')
ax[0].plot(t, head_pos[:,4]*-1000, 'b')
ax[0].set_ylabel('x [mm]', color='b')
ax[0].set_xlim((t[0], t[-1]))
ax1 = ax[0].twinx()
ax1.plot(t, coils, 'g', alpha=0.6)
ax1.set_ylim((0,10))
ax[1].plot(t, head_pos[:,5]*1000, 'b')
ax[1].set_ylabel('y [mm]', color='b')
ax[1].set_xlim((t[0], t[-1]))
ax2 = ax[1].twinx()
ax2.plot(t, coils, 'g', alpha=0.6)
ax2.set_ylabel('Sum of Channel Indices in Use (among 0 to 4)', color='g')
ax2.set_ylim((0,10))
ax[2].plot(t, head_pos[:,6]*-1000, 'b')
ax[2].set_ylabel('z [mm]', color='b')
ax[2].set_xlim((t[0], t[-1]))
ax[2].set_xlabel('time [s]')
ax3 = ax[2].twinx()
ax3.plot(t, coils, 'g', alpha=0.6)
ax3.set_ylim((0,10))
# The figure title should not overlap with the subplots.
fig.tight_layout(rect=[0, 0.03, 1, 0.95])
This problem has been solved after we inactivated mne.chpi._fit_chpi_quat_subset
called by mne.chpi.compute_head_pos
such that the head position at a given time point is estimated by all available HPI coils (i.e., which, at least, satisfy both gof_limit and dist_limit constraints).
Actual results
This is the figure showing the unstable head position estimation when mne.chpi._fit_chpi_quat_subset
is activated inside of mne.chpi.compute_head_pos
Additional information
Platform: Linux-5.10.0-19-amd64-x86_64-with-glibc2.31
Python: 3.10.5 | packaged by conda-forge | (main, Jun 14 2022, 07:04:59) [GCC 10.3.0]
Executable: /data/pt_01585/miniconda3/envs/mne_v110_baek/bin/python
CPU: : 32 cores
Memory: 503.5 GB
mne: 1.1.0
numpy: 1.22.4 {}
scipy: 1.8.1
matplotlib: 3.5.2 {backend=QtAgg}
sklearn: 1.1.2
numba: 0.56.0
nibabel: 4.0.1
nilearn: 0.9.2
dipy: 1.5.0
cupy: Not found
pandas: 1.4.3
pyvista: 0.36.1 {OpenGL 4.5 (Core Profile) Mesa 20.3.5 via llvmpipe (LLVM 11.0.1, 256 bits)}
pyvistaqt: 0.9.0
ipyvtklink: 0.2.2
vtk: 9.2.0.rc2
qtpy: 2.2.0 {PyQt5=5.15.2}
ipympl: Not found
pyqtgraph: 0.12.4
pooch: v1.6.0
mne_bids: Not found
mne_nirs: Not found
mne_features: Not found
mne_qt_browser: 0.3.1
mne_connectivity: Not found
mne_icalabel: Not found