'dev_head_t' and forward model computation(s)

Dear community,

I’m currently working on building an analysis pipeline for computing the MEG forward model, but I have some uncertainties regarding the role of info["dev_head_t"] in MNE.

In scenarios where MEG data are collected across multiple sessions for the same participant, each recording can have a different info["dev_head_t"] transformation (i.e., a different device-to-head transformation matrix).

If we use the default MEG-to-head transform in the destination parameter of mne.preprocessing.maxwell_filter, then — as I understand it — MNE will incorporate this transformation when computing the forward model.

Based on this, I assume that it would be incorrect to reuse a forward model computed with one info["dev_head_t"] for another recording that has a different dev_head_t, since the forward solution is dependent on the head position relative to the sensors. Is this assumption correct?

If so, does this mean we should compute a separate forward model for each unique info["dev_head_t"]? I’d appreciate confirmation or correction of this approach.

To illustrate my concern, here’s an example of code that shows a potentially incorrect usage: a forward model is computed from one head position but applied to data with a different dev_head_t:

import numpy as np
from matplotlib import pyplot as plt
import mne

from mne.datasets import testing
from mne.io import read_info

data_dir = testing.data_path(download=True)
subjects_dir = data_dir / "subjects"
sample_meg_dir = data_dir / "MEG" / "sample"
raw_fname = sample_meg_dir / "sample_audvis_trunc_raw.fif"
fwd_fname = sample_meg_dir / "sample_audvis_trunc-meg-eeg-oct-6-fwd.fif"
bem_fname = subjects_dir / "sample" / "bem" / "sample-320-320-320-bem-sol.fif"

# Load data
raw = mne.io.read_raw_fif(raw_fname, allow_maxshield=True, verbose=False)
fwd = mne.read_forward_solution(fwd_fname)

# First head position
info_1 = raw.info.copy()
fwd_1 = mne.make_forward_solution(info_1, fwd["mri_head_t"], fwd["src"], bem_fname)

# Simulate a second recording with a modified head position
info_2 = raw.info.copy()
trans = info_2["dev_head_t"]['trans'].copy()
trans[0, 0] += 1e-1  # Modify the transform slightly
info_2["dev_head_t"] = mne.transforms.Transform(1, 4, trans=trans)
fwd_2 = mne.make_forward_solution(info_2, fwd["mri_head_t"], fwd["src"], bem_fname)

# Apply forward model with mismatched head position
noise_cov = mne.make_ad_hoc_cov(info_1)
inverse = mne.minimum_norm.make_inverse_operator(
    info_1, fwd_2, noise_cov=noise_cov, loose="auto", depth=None, fixed=True
)
mne.minimum_norm.apply_inverse_raw(raw, inverse, lambda2=1.0 / 9.0)

Does this kind of mismatch invalidate the results ? If yes, wouldn’t it make sense for MNE to emit a warning or even an error in such cases (assuming that the necessary metadata survives up to that point to allow the verification)?

Any clarification or best practices would be greatly appreciated!

Best regards,


Related to this topic

  • Operating system: windows
  • MNE version : ‘0.20.dev0’

Based on this, I assume that it would be incorrect to reuse a forward model computed with one info["dev_head_t"] for another recording that has a different dev_head_t, since the forward solution is dependent on the head position relative to the sensors. Is this assumption correct?

I think so, to re-use the forward model you need the same dev_head_t. Which can be achieved by setting the destination parameter of MaxWell filter to the same value e.g. using (0, 0, 0.04) which corresponds to the default of the MEGIN software, or by providing a transform or the path to a transform file.
You could also use an average head position between N recordings to get something more realistic that a fix value, using mne.preprocessing.compute_average_dev_head_t — MNE 1.9.0 documentation

For this mismatch:

inverse = mne.minimum_norm.make_inverse_operator(
    info_1, fwd_2, noise_cov=noise_cov, loose="auto", depth=None, fixed=True
)

I don’t know how much it actually impacts the results, but I guess a warning would be a good addition, comparing the transform in info and in fwd. I don’t know if fwd["info"] still contains this information.

Mathieu

1 Like

Hey Mathieu,

Thanks for your response!

I’m already using mne.preprocessing.compute_average_dev_head_t to compute an average ["dev_head_t"] across all recordings, which I then use as the destination parameter in the Maxwell filter. So at the moment, I’m computing a single forward solution based on this aligned head position.

That said, I’ve been wondering how well the Maxwell filter handles projecting data to a different head position. I imagine the effectiveness depends on how far the original positions are from the target destination. In more extreme cases, this projection might not be sufficient — and using separate destinations (and thus separate forward solutions) per recording might yield better results. I’m not sure if there are any quantitative analyses on this topic — would be curious to hear if someone come across any.

After a quick check, I noticed that fwd["info"]["dev_head_t"] is still present. I think a warning could be valuable here, since mismatched dev_head_t transforms between the raw data and the forward solution can significantly affect the results. Here’s an updated script illustrating the effect of different dev_head_t values in inst['info'] and fwd['info']:

import numpy as np
from matplotlib import pyplot as plt
import mne
from mne.datasets import testing
from mne.io import read_info
from mne.viz.backends.renderer import backend
from mne.viz._brain.view import views_dicts
from mne.viz import set_3d_view
import seaborn as sns

data_dir = testing.data_path(download=True)
subjects_dir = data_dir / "subjects"
sample_meg_dir = data_dir / "MEG" / "sample"
raw_fname = sample_meg_dir / "sample_audvis_trunc_raw.fif"
fwd_fname = sample_meg_dir / "sample_audvis_trunc-meg-eeg-oct-6-fwd.fif"
trans_fname = sample_meg_dir / "sample_audvis_trunc-trans.fif"
mri_fname = subjects_dir / "sample" / "mri" / "T1.mgz"
bem = subjects_dir / "sample" / "bem" / "sample-320-320-320-bem-sol.fif"

fwd = mne.read_forward_solution(fwd_fname)
raw = mne.io.read_raw_fif(raw_fname, allow_maxshield=True, verbose=False)

info_1 = raw.info.copy()
fwd_1 = mne.make_forward_solution(info_1, fwd["mri_head_t"], fwd["src"], bem)

info_2 = raw.info.copy()
trans = info_1["dev_head_t"]['trans'].copy()
trans[0,-1] += 1e-2 # Add translation
info_2["dev_head_t"] = mne.transforms.Transform(1, 4, trans=trans)
fwd_2 = mne.make_forward_solution(info_2, fwd["mri_head_t"], fwd["src"], bem)

# Plot forward solutions
fig_1 = mne.viz.plot_alignment(trans=fwd_1["mri_head_t"], info=fwd_1["info"], subject="sample", subjects_dir=subjects_dir)
fig_2 = mne.viz.plot_alignment(trans=fwd_2["mri_head_t"], info=fwd_2["info"], subject="sample", subjects_dir=subjects_dir)

view = {'azimuth': 90.0, 'elevation': 90.0, 'focalpoint': 'auto', 'distance': 'auto'}

fig, axes = plt.subplots(1, 2, figsize=(10, 5))
for f,ax in zip([fig_1, fig_2], axes):
    set_3d_view(f, **view)
    im = backend._take_3d_screenshot(figure=f)
    ax.imshow(im)

# plot the effect on source time course
noise_cov = mne.make_ad_hoc_cov(info_1)
inverse_1 = mne.minimum_norm.make_inverse_operator(
    info_1, fwd_1, noise_cov=noise_cov, loose="auto", depth=None, fixed=True
    )

stc_1 = mne.minimum_norm.apply_inverse_raw(raw, inverse_1, lambda2=1.0 / 9.0)

inverse_2 = mne.minimum_norm.make_inverse_operator(
    info_1, fwd_2, noise_cov=noise_cov, loose="auto", depth=None, fixed=True
    )

stc_2 = mne.minimum_norm.apply_inverse_raw(raw, inverse_2, lambda2=1.0 / 9.0)

correlations = []
for i in range(0, stc_1.data.shape[0]):
  c = np.corrcoef(stc_1.data[i], stc_2.data[i])
  correlations.append(c[0,1])

plt.figure()
sns.displot(correlations)
plt.title("Correlation between source time courses")
plt.xlabel("Correlation")
plt.ylabel("Count (sources)")

@larsoner Maybe you want to chime in on this part?

That said, I’ve been wondering how well the Maxwell filter handles projecting data to a different head position. I imagine the effectiveness depends on how far the original positions are from the target destination. In more extreme cases, this projection might not be sufficient — and using separate destinations (and thus separate forward solutions) per recording might yield better results. I’m not sure if there are any quantitative analyses on this topic — would be curious to hear if someone come across any.

I haven’t read the whole thread but yes SNR changes when you move to a new virtual sensor / head position. When you move far the SNR generally suffers. Using different forward solutions could potentially help. I vaguely recall a poster from HBM or Biomag many years ago (5+?) where someone looked at computing a time-varying forward solution rather than using virtual sensors (kind of a more extreme / time-precise version of what you’re suggesting with the per-run forward) … but don’t remember how much it improved things :person_facepalming: But I imagine it probably became a paper at some point…