mne-tools / mne-nirs

Process Near-Infrared Spectroscopy Data in MNE
https://mne.tools/mne-nirs/
BSD 3-Clause "New" or "Revised" License
79 stars 35 forks source link

Snirf file assert failure #529

Closed alexk101 closed 6 months ago

alexk101 commented 7 months ago

Describe the bug

I am working with snirf files produced from a NirX Sport 2 system. I have validated the files against the official specification using (pysnirf2)[https://github.com/BUNPC/pysnirf2]. It shows that these are valid snirf files according to the official protocol. When attempting to read these files into mne using read_raw_snirf, an assertion is failed that causes the parsers to fail early. I haven't found any additional documentation to explain why this file isn't capable of being read. Regardless, the parser shouldn't fail to read a file that meets the official specification without explanation as to why (ie missing features). I include below the validator result. Additionally, the snirf file is following version 1.1 of the spec.

from snirf import validateSnirf

target = '2024-01-19_007.snirf'
result = validateSnirf(target)

>>> Found 564 OK      (hidden)
>>> Found 704 INFO    (hidden)
>>> Found 0 WARNING
>>> Found 0 FATAL  

>>> File is VALID

Looking into this problem myself, it seems that the assertion of the number of detector positions and number of detectors in the channel data does not always hold true. Looking at my data, the number of detector positions is 15, while the number of detectors found in the channel data is 14, where D8 is missing. Why this is the case for my data is something that I will have to investigate myself. However, this should be corrected for this package, as valid snirf files should never fail to be parsed without a reason. I also attach the relevant file.

2024-01-19_007.snirf.zip

Steps to reproduce

from mne.io import read_raw_snirf

target = '2024-01-19_007.snirf'
raw_intensity = read_raw_snirf(target, optode_frame='mri')

Expected results

A parsed snirf file as a RawSNIRF object.

Actual results

Traceback (most recent call last):
  File "/home/alexk101/Documents/Research/Bertenthal/fnirs/fnirs-nirx/src/process.py", line 83, in <module>
    process_all()
  File "/home/alexk101/Documents/Research/Bertenthal/fnirs/fnirs-nirx/src/process.py", line 78, in process_all
    sub_data[int(sub.stem)] = process_sub(snirf_file)
                              ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/alexk101/Documents/Research/Bertenthal/fnirs/fnirs-nirx/src/process.py", line 35, in process_sub
    raw_intensity = read_raw_snirf(target, optode_frame='mri')
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/alexk101/.mambaforge/envs/fnirs/lib/python3.11/site-packages/mne/io/snirf/_snirf.py", line 56, in read_raw_snirf
    return RawSNIRF(fname, optode_frame, preload, verbose)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<decorator-gen-340>", line 12, in __init__
  File "/home/alexk101/.mambaforge/envs/fnirs/lib/python3.11/site-packages/mne/io/snirf/_snirf.py", line 226, in __init__
    assert len(detectors) == detPos3D.shape[0]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError

Additional information

Platform             Linux-6.7.2-arch1-1-x86_64-with-glibc2.38
Python               3.11.6 | packaged by conda-forge | (main, Oct  3 2023, 10:40:35) [GCC 12.3.0]
Executable           /home/alexk101/.mambaforge/envs/fnirs/bin/python
CPU                   (16 cores)
Memory               30.5 GB

Core
├☒ mne               1.6.0 (outdated, release 1.6.1 is available!)
├☑ numpy             1.26.2 (OpenBLAS 0.3.25 with 16 threads)
├☑ scipy             1.11.4
├☑ matplotlib        3.8.2 (backend=QtAgg)
├☑ pooch             1.8.0
└☑ jinja2            3.1.2

Numerical (optional)
├☑ sklearn           1.3.2
├☑ numba             0.58.1
├☑ nibabel           5.1.0
├☑ nilearn           0.10.2
├☑ dipy              1.7.0
├☑ openmeeg          2.5.7
├☑ cupy              12.2.0
└☑ pandas            2.1.3

Visualization (optional)
├☑ pyvista           0.42.3 (OpenGL 4.5.0 NVIDIA 545.29.06 via NVIDIA GeForce RTX 3070 Ti Laptop GPU/PCIe/SSE2)
├☑ pyvistaqt         0.11.0
├☑ vtk               9.2.6
qt.qpa.plugin: Could not find the Qt platform plugin "wayland" in ""
├☑ qtpy              2.4.1 (PyQt5=5.15.8)
├☑ pyqtgraph         0.13.3
├☑ mne-qt-browser    0.6.1
├☑ ipywidgets        8.1.1
├☑ trame_client      2.13.0
├☑ trame_server      2.12.1
├☑ trame_vtk         2.6.2
├☑ trame_vuetify     2.3.1
└☐ unavailable       ipympl

Ecosystem (optional)
├☑ mne-nirs          0.6.0
└☐ unavailable       mne-bids, mne-features, mne-connectivity, mne-icalabel, mne-bids-pipeline
alexk101 commented 7 months ago

I have found where the problem comes from. In our optode layout, we have bundles of sources and detectors, 8 each. In this layout, we are using 16 sources and 15 detectors. However, because the sources and detectors are in these bundles, there is one remaining detector that would otherwise hang off the cap. To make this less of an obstruction, that detector is placed on the side of the cap, but is not used to capture any signal information. For that reason, it exists in the detector position array, but not in the channels. I have a fix for this by making the detectors array into a dictionary and can open a pull request if this solution is alright with the maintainers. Please let me know.

larsoner commented 7 months ago

A dictionary lookup seems reasonable to me! @rob-luke any opinion? If no response from @rob-luke by the end of the week @alexk101 feel free to open a PR and I'll look.

alexk101 commented 6 months ago

It looks like I put this in the wrong place. The error is actually in the main mne package, though this is an fnirs issue. I will reopen this issue there and reference this one