Skip to content
Open
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
fecef0b
ENH: add ctf275 montage
contsili Apr 7, 2025
81a81d0
ENH: add neuromag306 montage
contsili Apr 7, 2025
003a7ac
ENH: add ch_type in the Neuromag and CTF montages
contsili Apr 9, 2025
8bfba64
ENH: allow make_standard_montage to accept and parse the new meg mont…
contsili Apr 9, 2025
1857f14
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 9, 2025
73a5042
ENH: add neuromag and ctf to the builtin standard montages
contsili Apr 9, 2025
541078f
Merge branch 'axial_to_planar' of https://github.com/contsili/mne-pyt…
contsili Apr 9, 2025
61ba5a2
ENH: extend the initial setup/configuration of the function to MEG
contsili Apr 9, 2025
cb614d9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 9, 2025
aad0c4e
COS: comment
contsili Apr 9, 2025
0f956f7
Merge branch 'axial_to_planar' of https://github.com/contsili/mne-pyt…
contsili Apr 9, 2025
d4c043c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 9, 2025
10f8b7f
ENH: add example and TODOs
contsili Apr 9, 2025
a25eed0
Merge branch 'axial_to_planar' of https://github.com/contsili/mne-pyt…
contsili Apr 9, 2025
f36d9b1
ENH: added missing line in example
contsili Apr 9, 2025
23ba5cf
Merge remote-tracking branch 'upstream/main' into axial_to_planar
contsili Oct 11, 2025
ea76998
ENH: Add CTF and Neuromag montage CSV files
contsili Oct 12, 2025
bf411bf
ENH: Add read_meg_montage for loading MEG csv montages
contsili Oct 12, 2025
015113e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 12, 2025
e135ef9
ENH: Enhance interpolate_to to include MEG
contsili Oct 12, 2025
39f7046
Merge branch 'axial_to_planar' of https://github.com/contsili/mne-pyt…
contsili Oct 12, 2025
a589a4e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 12, 2025
1b3855d
ENH: Add MEG interpolation example
contsili Oct 12, 2025
253198c
Merge branch 'axial_to_planar' of https://github.com/contsili/mne-pyt…
contsili Oct 12, 2025
e2dd6cd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 12, 2025
6d0fc69
COS: Update example script docstrings to include MEG
contsili Oct 12, 2025
539b176
Merge branch 'axial_to_planar' of https://github.com/contsili/mne-pyt…
contsili Oct 12, 2025
221c357
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 12, 2025
bd99594
Merge remote-tracking branch 'upstream/main' into axial_to_planar
contsili Oct 12, 2025
9438af6
Merge branch 'axial_to_planar' of https://github.com/contsili/mne-pyt…
contsili Oct 12, 2025
82696c0
Merge remote-tracking branch 'upstream/main' into axial_to_planar
larsoner Oct 15, 2025
5eb71b3
FIX: Sty
larsoner Oct 15, 2025
0dc97c2
fix - do not handle ctf and neuromag as custom montages
contsili Oct 15, 2025
c8ea36e
Revert "fix - do not handle ctf and neuromag as custom montages"
contsili Oct 15, 2025
6bd0bc7
Revert "ENH: allow make_standard_montage to accept and parse the new …
contsili Oct 15, 2025
cb7b556
Revert "ENH: add neuromag and ctf to the builtin standard montages"
contsili Oct 15, 2025
0c4633a
Revert "ENH: add ch_type in the Neuromag and CTF montages"
contsili Oct 15, 2025
962db23
Revert "ENH: add neuromag306 montage"
contsili Oct 15, 2025
6dfdac5
Revert "ENH: add ctf275 montage"
contsili Oct 15, 2025
5e710fb
Merge branch 'main' into axial_to_planar
contsili Oct 15, 2025
7578e87
ENH: Refactor MEG interpolation example and update comments
contsili Oct 15, 2025
017b9d5
FIX: Import
larsoner Oct 16, 2025
d354597
enh - update built in montages
contsili Nov 19, 2025
bde04bc
enh - read csv manually without pandas
contsili Nov 19, 2025
c7fe986
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 19, 2025
8411fda
enh - create helper methods to make code more readable
contsili Nov 19, 2025
b4af9e4
Merge branch 'axial_to_planar' of https://github.com/contsili/mne-pyt…
contsili Nov 19, 2025
6e25fb4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 19, 2025
7c77fd2
enh - add docstring for verbose
contsili Nov 19, 2025
c10aa11
Merge branch 'axial_to_planar' of https://github.com/contsili/mne-pyt…
contsili Nov 19, 2025
d4bfc0e
Merge remote-tracking branch 'upstream/main' into axial_to_planar
contsili Nov 19, 2025
934e827
fix - remove ctf and neuromag from builtin montages
contsili Nov 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 73 additions & 4 deletions examples/preprocessing/interpolate_to.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,30 @@
.. _ex-interpolate-to-any-montage:

======================================================
Interpolate EEG data to any montage
Interpolate MEG or EEG data to any montage
======================================================

This example demonstrates how to interpolate EEG channels to match a given montage.
This can be useful for standardizing
This example demonstrates both EEG montage interpolation and MEG system
transformation.

For EEG, this can be useful for standardizing
EEG channel layouts across different datasets (see :footcite:`MellotEtAl2024`).

- Using the field interpolation for EEG data.
- Using the target montage "biosemi16".
- Using the MNE interpolation for MEG data to transform from Neuromag
(planar gradiometers and magnetometers) to CTF (axial gradiometers).


In this example, the data from the original EEG channels will be
In the first example, the data from the original EEG channels will be
interpolated onto the positions defined by the "biosemi16" montage.

In the second example, we will interpolate MEG data from a 306-sensor Neuromag
to 275-sensor CTF system.
"""

# Authors: Antoine Collas <[email protected]>
# Konstantinos Tsilimparis <[email protected]>
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.

Expand All @@ -30,6 +39,9 @@
ylim = (-10, 10)

# %%
# Part 1: EEG System Transformation
# ==================================

# Load EEG data
data_path = sample.data_path()
eeg_file_path = data_path / "MEG" / "sample" / "sample_audvis-ave.fif"
Expand Down Expand Up @@ -75,6 +87,63 @@
)
axs[2].set_title("Interpolated to Standard 1020 Montage using MNE interpolation")

# %%
# Part 2: MEG System Transformation
# ==================================
# We demonstrate transforming MEG data from Neuromag (planar gradiometers
# and magnetometers) to CTF (axial gradiometers) sensor configuration.

# Load the full evoked data with MEG channels
evoked_meg = mne.read_evokeds(
eeg_file_path, condition="Left Auditory", baseline=(None, 0)
)
evoked_meg.pick("meg")

print("Original Neuromag system:")
print(f" Number of magnetometers: {len(mne.pick_types(evoked_meg.info, meg='mag'))}")
print(f" Number of gradiometers: {len(mne.pick_types(evoked_meg.info, meg='grad'))}")

# %%
# Transform to CTF sensor configuration
# ======================================

# Interpolate Neuromag to CTF
evoked_ctf = evoked_meg.copy().interpolate_to("ctf275", mode="accurate")

print("\nTransformed to CTF system:")
print(f" Number of MEG channels: {len(mne.pick_types(evoked_ctf.info, meg=True))}")
print(f" Bad channels in original: {evoked_meg.info['bads']}")

# %%
# Compare evoked responses: Original Neuromag vs Transformed CTF
# The data should be similar but projected onto different sensor arrays

# Set consistent y-limits for comparison
ylim_meg = dict(grad=[-300, 300], mag=[-600, 600], meg=[-300, 300])

fig, axes = plt.subplots(3, 1, figsize=(10, 8), layout="constrained")

# Plot original Neuromag gradiometers
evoked_meg.copy().pick("grad").plot(
axes=axes[0], show=False, spatial_colors=True, ylim=ylim_meg, time_unit="s"
)
axes[0].set_title("Original Neuromag Planar Gradiometers", fontsize=14)


# Plot original Neuromag magnetometers
evoked_meg.copy().pick("mag").plot(
axes=axes[1], show=False, spatial_colors=True, ylim=ylim_meg, time_unit="s"
)
axes[1].set_title("Original Neuromag Magnetometers", fontsize=14)

# Plot transformed CTF gradiometers
evoked_ctf.plot(
axes=axes[2], show=False, spatial_colors=True, ylim=ylim_meg, time_unit="s"
)
axes[2].set_title("Transformed to CTF275 Axial Gradiometers", fontsize=14)

plt.show()

# %%
# References
# ----------
Expand Down
2 changes: 2 additions & 0 deletions mne/channels/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ __all__ = [
"read_dig_localite",
"read_dig_polhemus_isotrak",
"read_layout",
"read_meg_montage",
"read_polhemus_fastscan",
"read_vectorview_selection",
"rename_channels",
Expand Down Expand Up @@ -75,6 +76,7 @@ from .montage import (
read_dig_hpts,
read_dig_localite,
read_dig_polhemus_isotrak,
read_meg_montage,
read_polhemus_fastscan,
transform_to_head,
)
215 changes: 82 additions & 133 deletions mne/channels/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
pick_info,
pick_types,
)
from .._fiff.proj import _has_eeg_average_ref_proj, setup_proj
from .._fiff.proj import setup_proj
from .._fiff.reference import add_reference_channels, set_eeg_reference
from .._fiff.tag import _rename_list
from ..bem import _check_origin
Expand Down Expand Up @@ -968,161 +968,110 @@ def interpolate_bads(

return self

def interpolate_to(self, sensors, origin="auto", method="spline", reg=0.0):
"""Interpolate EEG data onto a new montage.
def interpolate_to(
self, sensors, origin="auto", method="MNE", mode="accurate", reg=0.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't change the method to "MNE" for EEG without a deprecation cycle. Let's make the default method=None which means under the hood (and in docs) "MNE for MEG sensors and spline for EEG".

):
"""Interpolate data onto a new sensor configuration.

.. warning::
Be careful, only EEG channels are interpolated. Other channel types are
not interpolated.
This method can interpolate EEG data onto a new montage or transform
MEG data to a different sensor configuration (e.g., Neuromag to CTF).

Parameters
----------
sensors : DigMontage
The target montage containing channel positions to interpolate onto.
sensors : DigMontage | str
For EEG: A DigMontage object containing target channel positions.
For MEG: A string specifying the target MEG system. Currently
supported: ``'neuromag'``, ``'ctf151'`` or ``'ctf275'``.
origin : array-like, shape (3,) | str
Origin of the sphere in the head coordinate frame and in meters.
Can be ``'auto'`` (default), which means a head-digitization-based
origin fit.
origin fit. Used for both EEG and MEG interpolation.
method : str
Method to use for EEG channels.
Supported methods are 'spline' (default) and 'MNE'.
Interpolation method to use.
For EEG: ``'MNE'`` (default) or ``'spline'``.
For MEG: ``'MNE'`` (default).
mode : str
Either ``'accurate'`` (default) or ``'fast'``, determines the
quality of the Legendre polynomial expansion used for
interpolation of MEG channels using the minimum-norm method.
Only used for MEG interpolation.
reg : float
The regularization parameter for the interpolation method
(only used when the method is 'spline').
The regularization parameter for the interpolation method.
Only used when ``method='spline'`` for EEG channels.

Returns
-------
inst : instance of Raw, Epochs, or Evoked
The instance with updated channel locations and data.
A new instance with interpolated data and updated channel
information.

Notes
-----
This method is useful for standardizing EEG layouts across datasets.
However, some attributes may be lost after interpolation.
**For EEG data:**

.. versionadded:: 1.10.0
"""
from ..epochs import BaseEpochs, EpochsArray
from ..evoked import Evoked, EvokedArray
from ..forward._field_interpolation import _map_meg_or_eeg_channels
from ..io import RawArray
from ..io.base import BaseRaw
from .interpolation import _make_interpolation_matrix
from .montage import DigMontage
This method interpolates EEG channels onto a new montage using
spherical splines or minimum-norm estimation. Non-EEG channels
are preserved without modification.

# Check that the method option is valid.
_check_option("method", method, ["spline", "MNE"])
_validate_type(sensors, DigMontage, "sensors")
**For MEG data:**

# Get target positions from the montage
ch_pos = sensors.get_positions().get("ch_pos", {})
target_ch_names = list(ch_pos.keys())
if not target_ch_names:
raise ValueError(
"The provided sensors configuration has no channel positions."
)
This method transforms MEG data to a different sensor configuration
using field interpolation.

# Get original channel order
orig_names = self.info["ch_names"]

# Identify EEG channel
picks_good_eeg = pick_types(self.info, meg=False, eeg=True, exclude="bads")
if len(picks_good_eeg) == 0:
raise ValueError("No good EEG channels available for interpolation.")
# Also get the full list of EEG channel indices (including bad channels)
picks_remove_eeg = pick_types(self.info, meg=False, eeg=True, exclude=[])
eeg_names_orig = [orig_names[i] for i in picks_remove_eeg]

# Identify non-EEG channels in original order
non_eeg_names_ordered = [ch for ch in orig_names if ch not in eeg_names_orig]

# Create destination info for new EEG channels
sfreq = self.info["sfreq"]
info_interp = create_info(
ch_names=target_ch_names,
sfreq=sfreq,
ch_types=["eeg"] * len(target_ch_names),
)
info_interp.set_montage(sensors)
info_interp["bads"] = [ch for ch in self.info["bads"] if ch in target_ch_names]
# Do not assign "projs" directly.

# Compute the interpolation mapping
if method == "spline":
origin_val = _check_origin(origin, self.info)
pos_from = self.info._get_channel_positions(picks_good_eeg) - origin_val
pos_to = np.stack(list(ch_pos.values()), axis=0)

def _check_pos_sphere(pos):
d = np.linalg.norm(pos, axis=-1)
d_norm = np.mean(d / np.mean(d))
if np.abs(1.0 - d_norm) > 0.1:
warn("Your spherical fit is poor; interpolation may be inaccurate.")

_check_pos_sphere(pos_from)
_check_pos_sphere(pos_to)
mapping = _make_interpolation_matrix(pos_from, pos_to, alpha=reg)
Common use cases for MEG transformation:

else:
assert method == "MNE"
info_eeg = pick_info(self.info, picks_good_eeg)
# If the original info has an average EEG reference projector but
# the destination info does not,
# update info_interp via a temporary RawArray.
if _has_eeg_average_ref_proj(self.info) and not _has_eeg_average_ref_proj(
info_interp
):
# Create dummy data: shape (n_channels, 1)
temp_data = np.zeros((len(info_interp["ch_names"]), 1))
temp_raw = RawArray(temp_data, info_interp, first_samp=0)
# Using the public API, add an average reference projector.
temp_raw.set_eeg_reference(
ref_channels="average", projection=True, verbose=False
)
# Extract the updated info.
info_interp = temp_raw.info
mapping = _map_meg_or_eeg_channels(
info_eeg, info_interp, mode="accurate", origin=origin
)
- Transform Neuromag data to CTF sensor layout for comparison
- Transform CTF data to Neuromag sensor layout
- Simulate what data would look like on a different MEG system

# Interpolate EEG data
data_good = self.get_data(picks=picks_good_eeg)
data_interp = mapping @ data_good
.. warning::
MEG field interpolation assumes that the head position relative
to the sensors is similar between systems. Large differences in
head position may affect interpolation accuracy.

# Create a new instance for the interpolated EEG channels
# TODO: Creating a new instance leads to a loss of information.
# We should consider updating the existing instance in the future
# by 1) drop channels, 2) add channels, 3) re-order channels.
if isinstance(self, BaseRaw):
inst_interp = RawArray(data_interp, info_interp, first_samp=self.first_samp)
elif isinstance(self, BaseEpochs):
inst_interp = EpochsArray(data_interp, info_interp)
else:
assert isinstance(self, Evoked)
inst_interp = EvokedArray(data_interp, info_interp)

# Merge only if non-EEG channels exist
if not non_eeg_names_ordered:
return inst_interp

inst_non_eeg = self.copy().pick(non_eeg_names_ordered).load_data()
inst_out = inst_non_eeg.add_channels([inst_interp], force_update_info=True)

# Reorder channels
# Insert the entire new EEG block at the position of the first EEG channel.
orig_names_arr = np.array(orig_names)
mask_eeg = np.isin(orig_names_arr, eeg_names_orig)
if mask_eeg.any():
first_eeg_index = np.where(mask_eeg)[0][0]
pre = orig_names_arr[:first_eeg_index]
new_eeg = np.array(info_interp["ch_names"])
post = orig_names_arr[first_eeg_index:]
post = post[~np.isin(orig_names_arr[first_eeg_index:], eeg_names_orig)]
new_order = np.concatenate((pre, new_eeg, post)).tolist()
else:
new_order = orig_names
inst_out.reorder_channels(new_order)
return inst_out
.. versionadded:: 1.10.0
"""
from .interpolation import _interpolate_to_eeg, _interpolate_to_meg
from .montage import DigMontage

_check_preload(self, "interpolation")

# Determine if we're doing EEG or MEG interpolation
is_meg_interpolation = isinstance(sensors, str)
is_eeg_interpolation = isinstance(sensors, DigMontage)

if is_eeg_interpolation:
# Check that the method option is valid.
_validate_type(sensors, DigMontage, "sensors")
Comment on lines +1043 to +1045
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes here are mostly whitespace and nest things in conditionals. Rather than doing this, would it be possible to do something like

def interpolate_to(...)
    ...
    if is_eeg_interpolation:
        self._interpolate_to_eeg(...)  # which contains most of the old code, unmodified
    if is_meg_interpolation:
        self._interpolate_to_meg(...)   # which contains most of the new code

This separates out the functionality a bit in a more readable way, and improves git blame

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved EEG and MEG interpolation code from InterpolationMixin in channels.py to dedicated helper functions (_interpolate_to_eeg and _interpolate_to_meg) in interpolation.py.


method = _handle_default("interpolation_method", method)

# Filter method to only include 'eeg' and 'meg'
supported_ch_types = ["eeg", "meg"]
keys_to_delete = [key for key in method if key not in supported_ch_types]
for key in keys_to_delete:
del method[key]

# Force MEG to always use MNE method,
# otherwise when method = "spline", the _handle_default function
# forces all channel types to use that method
if "meg" in method:
method["meg"] = "MNE"
valids = {"eeg": ("spline", "MNE"), "meg": ("MNE")}
for key in method:
_check_option("method[key]", key, tuple(valids))
_check_option(f"method['{key}']", method[key], valids[key])
logger.info("Setting channel interpolation method to %s.", method)

return _interpolate_to_eeg(self, sensors, origin, method, reg)

elif is_meg_interpolation:
# MEG interpolation to canonical sensor configuration
_check_option("sensors", sensors, ["neuromag", "ctf151", "ctf275"])
_check_option("method", method, ["MNE"])
_check_option("mode", mode, ["accurate", "fast"])

return _interpolate_to_meg(self, sensors, origin, mode)


@verbose
Expand Down
Loading
Loading