Montage either not saved in fif or not read

Hi everyone!

I am working with fNIRS-data collected form NIRSport 2 device.
Python version: 3.9
mne version: 0.23
OS: macOS BigSur

Loading the snirf files and creating a custom montage works fine, as well as adding the montage into the raw mne object. Saving the raw object into fif format files afterwards runs without errors or warning.
The problem is, that the montage information, I added to the raw objects before, is missing when I load these. Is this normal behaviour or am I missing something?

Here’s a short code example:

sub_raw = mne.io.read_raw_snirf(filename).load_data()
montage_path = os.path.join(os.path.split(filename)[0], 'digpts.csv')
montage = mne.channels.read_custom_montage(montage_path,head_size=None)
sub_raw.set_montage(montage) 
sub_raw.info.set_montage(montage)
# montage information is now added to raw object
sub_raw.get_montage() # is not empty
fif_path = os.path.join(outpath,fname.replace('.','-') + '_raw.fif'
sub_raw.save(fif_path),overwrite=True)

#loading of fif files
raw_fif = mne.io.read_raw_fif(fif_path,preload=True)
sub_raw.get_montage() # is empty list

Hi sans-dev,

Thanks for the clear report.

I can confirm that the get_montage() returns an empty list as you report. I have created a fix over at MRG: Test get_montage on fNIRS data by rob-luke · Pull Request #9524 · mne-tools/mne-python · GitHub. Once my fix is reviewed and merged I will report back here and check it works for you.

Please let us know of any other hiccups you encounter.
Regards,
Rob


However, I am not sure that this will be an issue. I think we can optimise your code somewhat, and you may not even need the load and get montage functions (maybe). I am not an expert with the get_montage function, so lets work through this together.

FIrst of all, do you need to load the montage as you are doing? The NIRSport2 device should store the optode locations in the SNIRF file. If you load the data as below then it should map all the locations correctly. Try this…

sub_raw = mne.io.read_raw_snirf(filename, optode_frame="mri").load_data()

I dont have your data, but using some data I collected this little snippet shows the correct channel (midpoint between source and detector) locations.

import mne
import os.path as op
import os
from mne.datasets.testing import data_path

filename = op.join(data_path(download=True),
                             'SNIRF', 'NIRx', 'NIRSport2', '1.0.3',
                             '2021-05-05_001.snirf')

sub_raw = mne.io.read_raw_snirf(filename, optode_frame="mri").load_data()
sub_raw.plot_sensors()

which displays the correct locations for this test data.

image

Could you please confirm your channels are loaded by using the plot_sensors function? If so, will you need the get_montage function? Of course when the above fix is merged you can use the get_montage function as desired.

1 Like

NIRx use the MRI coorinate frame, so you need to include that. See the example code above.

Please let me know if this information was not easily accessible to you and we can try and improve the documentation.

Hi Rob,
thanks for your comprehensive and detailed response. The ‘optode_frame’ seems to be not integrated into to the newest stable release of mne-python, thus it’s not part of the documentation. I’ll install the dev0 branch now and test your example on my data.

Indeed, I had forgotten this. Thanks for reminding me. The fNIRS code is more recent than much of the meeg code, so many features may only be available on the development branch (main). Although things are quickly becoming more stable as people such as yourself report these bugs. Thanks again. And I’ll ping back here once that big fix is merged.

Could you please confirm your channels are loaded by using the plot_sensors function?

Plotting the sensors works fine on the development branch using your code snippet:

Plotting the sensors with:

sub_raw.plot_sensors()

displayed this topology.

I am a bit confused about the sources and detectors which are plotted outside of the head model (there are also some outside in your example). Is this due to a wrongly shifted and scaled head plot?

Besides that, I think I am good to go with your solution and don’t need the get_montage() function right now. Thank you very much for your help and the fast fix!

Good question. I always assumed this was due to some approximation to generate the 2d representation. I’d be keen for someone to confirm this though. @drammock do you know if this is the case?

I always check the 3D positions using something like this code. Then I know it’s correct. See Preprocessing functional near-infrared spectroscopy (fNIRS) data — MNE 0.23.0 documentation

My pleasure. We will still fix the get montage code in the coming days. Thanks for reporting it.

If you think of the head outline as the equator, then points outside the head are sources/detectors below the equator. E.g. if the equator lands at mid-forehead level then an electric on the temple would show up outside the head. So yes, it relates to the distortion of representing 3d positions in 2d.

Thanks for the explanation and clarification. I will check the montage via 3D plot, as @rob-luke mentioned.

Thanks for the explanation @drammock that makes sense.

I’m glad we could help you @sans-dev. Also check out MNE-NIRS MNE-NIRS — MNE-NIRS 0.0.6 dev documentation for some more fNIRS specific analysis functions and examples.

Hi @sans-dev,

Just closing this conversation out by saying that the get_montage() code fix has now been accepted and is available in the main branch. Just in case you need it in the future.

Thanks for the report,
Rob

Hi @rob-luke ,

great, that it’s fixed already! Thanks a lot!

Cheers,
Sebastian