Skip to content

Axial to planar gradiometer transformation - #13196

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

larsoner merged 61 commits into
mne-tools:mainfrom
contsili:axial_to_planar

Conversation

@contsili

@contsili contsili commented Apr 7, 2025

Copy link
Copy Markdown
Contributor

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

welcome Bot commented Apr 7, 2025

Copy link
Copy Markdown

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

larsoner commented Apr 9, 2025

Copy link
Copy Markdown
Member

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

@contsili

contsili commented Apr 9, 2025 •

Copy link
Copy Markdown
Contributor Author

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.
@larsoner

Copy link
Copy Markdown
Member

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

@contsili

contsili commented Jan 29, 2026 •

Copy link
Copy Markdown
Contributor Author

@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)
  ...

@larsoner larsoner left a comment

Copy link
Copy Markdown
Member

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
Copy Markdown
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
Copy Markdown
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)

@larsoner larsoner left a comment

Copy link
Copy Markdown
Member

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

contsili commented Feb 4, 2026

Copy link
Copy Markdown
Contributor Author

@larsoner thank you for the finishing touches!

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

welcome Bot commented Feb 4, 2026

Copy link
Copy Markdown

🎉 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)
wmvanvliet pushed a commit to wmvanvliet/mne-python that referenced this pull request Feb 9, 2026
Co-authored-by: Konstantinos Tsilimparis <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Eric Larson <[email protected]>
@chsquare

Copy link
Copy Markdown
Contributor

Hi all! Thanks for implementing this transformation! I just noticed that interpolate_to() resets sampling frequency, filter info, and other information which should instead be maintained across the interpolation step. I will open pull request containing a fix.

sseth pushed a commit to xannnimal/mne-python that referenced this pull request Mar 25, 2026
Co-authored-by: Konstantinos Tsilimparis <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Eric Larson <[email protected]>
@dieloz

dieloz commented Oct 5, 2026

Copy link
Copy Markdown

Dear all,
Thanks for the fix! I have reviewed the code and I was wondering whether the equivalent of ft_megplanar's sincos method is implemented in "interpolate_to" to generate topoplots like this. This synthetic planar gradient is routinely performed in CTF systems.
I came across issue #9609 and wanted to check if a sincos implementation exists anywhere in MNE-Python.
Thanks in advance,
Diego

@chsquare

chsquare commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Hi Diego,
As you probably saw yourself, the actual work is done by mne.forward._field_interpolation. _map_meg_or_eeg_channels() and as far as I see there is no sincos-like method. The axial to planar transformation achieved here switches from a CTF to a neuromag layout, which might actually change the sensor positions, so even there was a sincos method, it might not result in the same topoplot you get with ft_megplanar. I guess you can define an info object that contains sensor position informations in a way that result in a fieldtrip-like sincos planar CTF output and read in this info object before calling _interpolate_to_meg() instead of the default neuromag one - but others probably know better, e.g. @larsoner ?

@larsoner

larsoner commented Oct 6, 2026

Copy link
Copy Markdown
Member

No IIRC we only support sourceproject equiv in MNE-Python. I think this is mostly because we haven't seen a need for it yet. @chsquare you need it for your analyses and expect it to give a different (maybe better) result than minimum-norm based transformation?

@dieloz

dieloz commented Oct 6, 2026

Copy link
Copy Markdown

Thank you @chsquare and @larsoner for your replies. In fieldtrip we routinely compute the synthetic planar gradient because the strongest field is usually situated above the neural sources. It's a kind of "crude" source reconstruction that aligns very well across participants. No worries if nothing is implemented yet!

@larsoner

larsoner commented Oct 6, 2026

Copy link
Copy Markdown
Member

So would a evoked.interpolate_to("planar") that mapped axial to planar grad pair work for you? I don't think we need the sincos method for this as minimum norm could still get us there

@dieloz

dieloz commented Oct 6, 2026

Copy link
Copy Markdown

Thank you @larsoner ! I didn't know this option and I'll definitely check it out

@larsoner

larsoner commented Oct 6, 2026

Copy link
Copy Markdown
Member

It doesn't exist yet, but I could add it. Would it take care of your use case?

@dieloz

dieloz commented Oct 6, 2026

Copy link
Copy Markdown

As long as the synthetic planar gradient is generated taking into account the neighboring axials, that would cover my use case perfectly. In any case, if you implement this axial-to-planar interpolation, I can compare it to the FieldTrip sincos equivalent myself making a pull request in the following months.
Thanks a lot!

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

4 participants