Skip to content

Axial to planar gradiometer transformation#13196

Merged
larsoner merged 61 commits intomne-tools:mainfrom
contsili:axial_to_planar
Feb 4, 2026
Merged

Axial to planar gradiometer transformation#13196
larsoner merged 61 commits intomne-tools:mainfrom
contsili:axial_to_planar

Conversation

@contsili
Copy link
Contributor

@contsili contsili commented Apr 7, 2025

Reference issue (if any)

Fixes #9609 .

What does this implement/fix?

  • Store the canonical CTF and Neuromag sensor definitions in a txt file
  • Extent interpolate_to() to MEG sensors to accommodate axial to planar gradiometer transformation (and back)
  • Since interpolate_to() works similarly to interpolate_bads() I suggest we create two new functions in interpolation.py: _interpolate_to_meeg that uses the 'MNE' interpolation method

Additional information

The channel positions and orientations are adopted from fieldtrip: https://github.com/fieldtrip/fieldtrip/blob/master/template/gradiometer

I am not sure if for the interpolation we need the coil positions and orientations. For clarity: a gradiometer is ONE channel but has TWO coils. A magnetometer is ONE channel and has ONE coil.

As an expectation I have that I will plot ERPs in the ctf and neuromag format. Also I want to create topoplots like: https://www.fieldtriptoolbox.org/assets/img/tutorial/eventrelatedaveraging/figure8.png

Konstantinos Tsilimparis added 2 commits April 7, 2025 17:26
This is based on the channel positions and orientations provided by fieldtrip: https://github.com/fieldtrip/fieldtrip/blob/master/template/gradiometer/ctf275.mat
This is based on the channel positions and orientations provided by fieldtrip: https://github.com/fieldtrip/fieldtrip/blob/master/template/gradiometer/neuromag306.mat
@welcome
Copy link

welcome bot commented Apr 7, 2025

Hello! 👋 Thanks for opening your first pull request here! ❤️ We will try to get back to you soon. 🚴

@contsili contsili marked this pull request as draft April 7, 2025 15:57
@larsoner
Copy link
Member

larsoner commented Apr 9, 2025

Looks like you're making some progress, let me know when you'd like some feedback!

@contsili
Copy link
Contributor Author

contsili commented Apr 9, 2025

Hi @larsoner! After a while, I finally found some time :)

I think this is a good moment for some feedback.

Issues / Questions I encountered:
1. Handling ch_type when interpolating from CTF → Neuromag

My initial understanding was that when we interpolate from CTF to Neuromag, the ch_type should already be known.

This is because, CTF systems have reference sensors that should not be interpolated and Neuromag systems have gradiometers and magnetometers, so we need to know the type of each channel.

That's why I added the ch_type parameter in the .txt montage files — to explicitly specify this.

2. Scope of interpolation: only gradiometers?
In relation to (1), I assumed we would only interpolate gradiometers (i.e., axial ↔ planar), and not magnetometers.

Based on this, I thought I would input in _map_eeg_and_meg_channels would use info_from and info_to objects that contain only the gradiometer channels. This was another reason for adding the ch_type parameter in the montage.

3. Limitations of make_dig_montage
All the montages are created via make_dig_montage.

From the documentation of make_dig_montage:

 .. note::
            For custom montages without fiducials, this parameter must be set
            to ``'head'``.

I understood that for my setup (no fiducials) I should use a custom montage.

However: running montage = make_dig_montage(ch_pos=ch_pos, coord_frame="head") does not parse the ch_type or ori and my channels are read as generic EEG labels, e.g., standard_montage.ch_names[0] = 'EEG #1' etc.

4. Custom _meg() function and related problems
Because of (3), I created a custom_meg()function to parse the .txt files.

But now I run into new problems. When I run interpolate_to() :

_validate_type(sensors, DigMontage, "sensors") leads to the error: AttributeError: 'CustomMontage' object has no attribute 'get_positions'
ch_pos = sensors.get_positions().get("ch_pos", {}) leads to the error: sensors must be an instance of DigMontage, got <class 'mne.channels._standard_montage_utils._meg.<locals>.CustomMontage'> instead

5. I added _meg() inside _standard_montage_utils but am I allowed to change such private function ?

6. I am trying to follow how interpolate_to() code style and structure is already built.

However, this forces us to diverge a bit from the standard interpolate_bads recipe

One idea I had would be to encapsulate some logic in a new function interpolate_to_meeg() inside interpolation.py. Just as interpolate_bads_meeg() works inside interpolation.py. But if we do that then we need to do the same thing for the EEG part of interpolate_to()

@contsili contsili marked this pull request as ready for review April 9, 2025 15:48
@contsili contsili requested a review from drammock as a code owner April 9, 2025 15:48
@larsoner larsoner added this to the 1.11 milestone Jun 26, 2025
Konstantinos Tsilimparis and others added 4 commits October 11, 2025 20:09
Added new montage data files for CTF151, CTF275, and Neuromag306 systems in CSV format to mne/channels/data/montages/. These files provide sensor location and orientation information.
Introduces the read_meg_montage function to load canonical MEG sensor positions and orientations from CSV files for supported systems ('neuromag', 'ctf151', 'ctf275'). This utility constructs an Info object with sensor metadata for use in field interpolation.
@contsili
Copy link
Contributor Author

@contsili do you have time to work on this? If it would help I could push some commits as well

I was on holidays. I will work on it in the next two weeks and then you can take over to do the final adjustments if necessary

Konstantinos Tsilimparis and others added 4 commits November 19, 2025 17:48
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.
@contsili contsili requested a review from larsoner November 19, 2025 17:04
Copy link
Member

@larsoner larsoner left a comment

Choose a reason for hiding this comment

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

Just a couple of preliminary comments -- can you see if you can get CIs happy?

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".

inst : instance of Raw, Epochs, or Evoked
A new instance with interpolated data.
"""
from .._fiff.meas_info import create_info
Copy link
Member

Choose a reason for hiding this comment

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

CIs complain that some imports are nested that should not be etc.

@larsoner
Copy link
Member

@contsili would it help if I pushed some commits here? Would be good to get this in!

@contsili
Copy link
Contributor Author

contsili commented Jan 29, 2026

@larsoner yes it would help! The implementation is working, the only thing to be done is see why the tests are failing and adjust the code.

Once you are done, I can chime in and finish whatever is left :)

* upstream/main: (67 commits)
  DOC: Add jupyterlite idea to roadmap (mne-tools#13620)
  MAINT: Use f-strings in test_import_nesting.py (mne-tools#13551)
  Improve docs for raw.to_data_frame (mne-tools#13590)
  Sensitivity map doc improved (mne-tools#13578)
  [pre-commit.ci] pre-commit autoupdate (mne-tools#13612)
  MAINT: Fix for latest SciPy (mne-tools#13613)
  Fix pre-commit call in SPEC0 action [ci skip] (mne-tools#13609)
  MAINT: Add mne-denoise to CI dependencies (mne-tools#13607)
  FIX: do not cache canvas object (mne-tools#13606)
  FIX: Set calibration plot axes to screen resolution if available (mne-tools#13558)
  Refactoring eyetracking.py (mne-tools#13602)
  [pre-commit.ci] pre-commit autoupdate (mne-tools#13601)
  Follow up PR to PR - mne-tools#13596 (mne-tools#13599)
  FIX: Sphinx (mne-tools#13600)
  Doc improvement - Examples using <some-method> section quirk fix (mne-tools#13596)
  Add more information to eSSS in examples and docsstring (mne-tools#13591)
  np.fix -> np.trunc (deprecation) (mne-tools#13594)
  Make mne.sys_info() work with powershell 7+ (mne-tools#13593)
  BUG: Fix minor bug with T1 check (mne-tools#13588)
  [pre-commit.ci] pre-commit autoupdate (mne-tools#13587)
  ...
Copy link
Member

@larsoner larsoner left a comment

Choose a reason for hiding this comment

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

Okay @contsili existing tests should pass

Do you want to take a stab at adding tests? Or would it help if I did it?

@@ -0,0 +1,152 @@
name,coil_type,x,y,z,ex_x,ex_y,ex_z,ey_x,ey_y,ey_z,ez_x,ez_y,ez_z
Copy link
Member

Choose a reason for hiding this comment

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

@contsili I moved these to their own directory

I also renamed read_meg_montage to read_meg_canonical_info since it returns an Info not a montage

info_eeg, info_to, mode="accurate", origin=origin
)

return _remap_add(inst, mapping, info_to, ch_type="eeg")
Copy link
Member

Choose a reason for hiding this comment

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

@contsili some simplification + DRY-ing of code here -- once you have the mapping infos and the channel type to interp, the operations should be the same (including where the new channels get inserted)

Copy link
Member

@larsoner larsoner left a comment

Choose a reason for hiding this comment

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

Pushed a commit with a test, I'll just take a look at the example output and make sure it's good then merge! Thanks in advarce @contsili !

@contsili
Copy link
Contributor Author

contsili commented Feb 4, 2026

@larsoner thank you for the finishing touches!

@larsoner larsoner merged commit 437d79b into mne-tools:main Feb 4, 2026
32 checks passed
@welcome
Copy link

welcome bot commented Feb 4, 2026

🎉 Congrats on merging your first pull request! 🥳 Looking forward to seeing more from you in the future! 💪

larsoner added a commit that referenced this pull request Feb 5, 2026
* upstream/main:
  Axial to planar gradiometer transformation (#13196)
  MAINT: Work around pandas deprecation (#13632)
  FIX: Handle empty landmarkLabels in read_raw_snirf (#13628)
  Implement the MEF3 support (#13610)
  BUG: Fix bug with somato dataset paths (#13630)
  DOC: Move mne-kit-gui (#13629)
  fix team name for inactivity tool [ci skip] (#13626)
  MAINT: Update dependency specifiers (#13625)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Axial to planar gradiometer transformation

2 participants