-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
Describe the new feature or enhancement
Goal: extend MNE-Python's NIRS processing to support an arbitrary number of wavelengths (≥2) instead of being limited (hard-coded in several places) to two.
The same idea has been raised in issue #9816 with the main difference that my issue is in preparation for a PR rather than a request. I'd like to discuss implementation details and to explore devs' opinion on how feasible such changes would be.
Please check the linked issue for additional information about why this would be a good idea. In short, (some) Shimadzu fNIRS devices work with 3 wavelengths, and in the future there might be other devices with more, but mne's SNIRF I/O and processing functions are coded to accept only 2 wavelengths.
From my perspective, these changes would be mostly beneficial as I'm using a Shimadzu device. While the device doesn't support .snirf output, there are ways to convert the data to that format. And while it's possible to preprocess and convert to Hb data first in some other tool(s) and then import to mne, it would be ideal to keep the entire processing pipeline to one tool.
Describe your proposed implementation
I've done some exploration in the codebase to identify relevant bits of code, both manually and with the help of Claude 4. Based on this, it seems that relevant parts of the code are in SNIRF I/O (mne.io.snirf) and NIRS preprocessing (mne.preprocessing.nirs). I'm sure some tests will need to be updated as well, but I haven't yet spent time on them.
I actually have a preliminary implementation of the described changes that you can see at my fork. That code is untested, but it should provide a general impression of the extent and complexity of required changes. Of course, tests will need to be added/updated in addition.
Major changes
- Channel data structure: with 2 wavelengths, channel data is stored as ABAB... . All functions handling OD and amplitude data will need to be updated not to rely on a fixed [::2], [1::2] sequence. I listed this as a major change due to the difficulty of locating every function that works this way. Claude thinks it's been able to find all of them (see list of files to be changed below). It still requires devs' experience to know if there are some I might not have been able to find.
- Beer-Lambert calculation (algorithmic change): The current implementation uses pseudo-inverse
pinv()
to calculateΔOD = E × L × PPF × Δc
. I think it's possible to update this with minimal changes, just changing the EL matrix to n x 2 where n is the number of wavelengths, instead of the current 2 x 2. After Beer-Lambert, excess channels will need to be dropped. - Scalp coupling index (algorithmic change): I'm not aware of any reports where SCI was defined for >2 wavelengths. My idea here is to calculate all possible pairs of correlation, and use the minimum value. Intuitively, this should be correct as all wavelength channels should be correlated.
I'm hoping to receive feedback on the correctness of the above proposed algorithmic changes. For n=2 wavelengths, the new calculation methods wouldn't introduce any new changes (E in Beer-Lambert would still be 2x2, and there is only one correlation pair so the minimum is irrelevant), and if necessary, the current implementation could be preserved in a separate code path.
Minor changes
- SNIRF file reading: checks and validation steps need to be updated. While there's a number of validation checks, the overall complexity of the code changes isn't high.
List of affected files and functions:
- /mne/io/snirf/_snirf.py:
class RawSNIRF(BaseRaw)
has a simple check for 2 wavelengths and an error message. - /mne/preprocessing/nirs/_beer_lambert_law.py:
beer_lambert_law()
logic needs to be updated to >=2 wavelengths, plus it will need to remove excess channels after assigning HbO/R data to the first 2. - /mne/preprocessing/nirs/_beer_lambert_law.py:
_load_absorption()
is hard-coded to 2 wavelengths. - /mne/preprocessing/nirs/_scalp_coupling_index.py:
scalp_coupling_index()
needs an algorithmic update - /mne/preprocessing/nirs/nirs.py:
_check_channels_ordered()
has various validation checks and_fnirs_spread_bads()
needs to spread bad markings over all wavelengths in group
Describe possible alternatives
The algorithmic changes (Beer-Lambert and SCI) could be implemented in another way. The Beer-Lambert pseudo-inverse should work with n>2 with minimal changes and the change seems mathematically correct. The SCI calculation using the minumum of all correlations appers to be the most conservative in terms of being able to spot any coupling issues, but without studies to rely on, it should be discussed.
Additional context
The project mne-python will also require some changes, which will I will discuss in another issue opened there.