Skip to content

Conversation

@contsili
Copy link

@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

contsili 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
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
contsili 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
Author

@larsoner, @britta-wstnr this pr is ready for review. :)

The interpolation from Neuromag to CTF works! (see the example script I uploaded).

Quite some github actions seem to fail, let me know if I can fix something to let them pass.

Notes:

  • I could not find a dataset with 275 sensors in the ctf system. The best I could is to get 274 sensors (from an open dataset available in spm, from: https://www.fil.ion.ucl.ac.uk/spm/download/data/mmfaces/ I got the http://multimodal_meg.zip file)
  • I thought the best location for read_meg_montage is in montages.py as the MEG csv files are kind of "montages". What do you think?
  • Shall I add tests? or is the example script enough?

@larsoner
Copy link
Member

Quite some github actions seem to fail, let me know if I can fix something to let them pass.

At least as a first step I'll merge main into this branch and push a commit to get style CIs happy. Until they're happy a lot of CIs won't bother running

* upstream/main:
  MAINT: Auth [skip azp] [skip actions]
  MAINT: Deploy [circle deploy] [skip azp] [skip actions]
  Bump github/codeql-action from 3 to 4 in the actions group (mne-tools#13442)
  ENH: Dont constrain fiducial clicks to mesh vertices (mne-tools#13445)
  Use timezone-aware ISO 8601 for website timestamp (mne-tools#13347)
  [pre-commit.ci] pre-commit autoupdate (mne-tools#13443)
  FIX: Update osf.io links to new format (mne-tools#13440)
@larsoner
Copy link
Member

Shall I add tests? or is the example script enough?

I think ideally here you would modify some example, and some tests. For a simple test, if you take Neuromag data, transform it to CTF, and back to the original Neuromag info / dev_head_t, are the start and end data very highly correlated?

I would also check that the montages loaded for Neuromag match the .info from some test file up to numerical precision. Same thing for any other systems you add.

For failed tests you should be able to click on the name of any failed CI. So for example "ubuntu-latest pip" which I know runs almost all the tests, you can see a few failures:

https://github.com/mne-tools/mne-python/actions/runs/18536524047/job/52833052296?pr=13196#step:19:6190

@contsili
Copy link
Author

contsili commented Oct 15, 2025

I think ideally here you would modify some example, and some tests. For a simple test, if you take Neuromag data, transform it to CTF, and back to the original Neuromag info / dev_head_t, are the start and end data very highly correlated?

I will add tests for that

I would also check that the montages loaded for Neuromag match the .info from some test file up to numerical precision. Same thing for any other systems you add.

Do you have a similar test for EEG montages, just to get some inspiration?

I generated all the MEG montages directly from the .info component after loading the dataset in mne, so every MEG montage and .info contain identical sensor positions.

The Neuromag306 and CTF151 datasets I used come from the MNE-Python sample data, so I trust their .info fields to be accurate. For the CTF275, I performed a small visual check to verify that the dataset I received from UCL/SPM has the correct sensor positions for the CTF275 system. I compared the CTF275 data from the UCL/SPM dataset with a Donders dataset I have, and the gradiometer positions differed only by about a millimeter.

@contsili

This comment was marked as resolved.

@contsili
Copy link
Author

I had forgotten some old code in this PR. I reverted all the commits that had this code. Now let's wait and see if the tests will pass ;)

contsili and others added 2 commits October 16, 2025 00:38
Reorganized the code to separate EEG and MEG system transformation sections.
Comment on lines +1728 to +1729
# TODO: Refactor not to use Pandas (should be able to parse without it)
df = pd.read_csv(csv_file)
Copy link
Member

Choose a reason for hiding this comment

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

pandas is an optional dependency of MNE and not everyone will have it. Is it easy enough to just parse the CSV directly / manually? Usually we try to do that if it's not too much work

Comment on lines +1048 to +1050
if is_eeg_interpolation:
# Check that the method option is valid.
_validate_type(sensors, DigMontage, "sensors")
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

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