mne-python icon indicating copy to clipboard operation
mne-python copied to clipboard

Axial to planar gradiometer transformation

Open contsili opened this issue 7 months ago • 3 comments

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 avatar Apr 07 '25 15:04 contsili

Hello! šŸ‘‹ Thanks for opening your first pull request here! ā¤ļø We will try to get back to you soon. 🚓

welcome[bot] avatar Apr 07 '25 15:04 welcome[bot]

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

larsoner avatar Apr 09 '25 15:04 larsoner

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 avatar Apr 09 '25 15:04 contsili

@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?

contsili avatar Oct 12 '25 15:10 contsili

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

larsoner avatar Oct 15 '25 16:10 larsoner

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

larsoner avatar Oct 15 '25 20:10 larsoner

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 avatar Oct 15 '25 21:10 contsili

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

The tests fail because the neuromag and ctf montages do not have the dig attribute:

FAILED mne/channels/tests/test_standard_montage.py::test_standard_montages_have_fids[ctf275] - AttributeError: 'CustomMontage' object has no attribute 'dig'

I think I need to make some changes to read_meg_montage to fix this. Would it make sense to add read_meg_montage directly in channels.py as a helper function, since MEG sensors are not real montages and should not be handled as such within montage.py? This seems to me the most straightforward solution, but I’d like your advice since you know the MNE architecture better.

contsili avatar Oct 15 '25 22:10 contsili

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 avatar Oct 15 '25 22:10 contsili

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

larsoner avatar Nov 05 '25 18:11 larsoner

@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

contsili avatar Nov 14 '25 08:11 contsili