-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
ENH: Support for Multi-Wavelength (>2) NIRS/SNIRF Data Processing #13408
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
… in np.zeros so there was need to write 0 to it when correlation was infinite.
…ments to show correct return format
…ode over several lines
Hello! 👋 Thanks for opening your first pull request here! ❤️ We will try to get back to you soon. 🚴 |
for more information, see https://pre-commit.ci
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a reasonable start to me!
|
||
def _load_absorption(freqs): | ||
"""Load molar extinction coefficients.""" | ||
"""Load molar extinction coefficients""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a minor style regression
"""Load molar extinction coefficients""" | |
"""Load molar extinction coefficients.""" |
|
||
# freqs = np.array([raw.info["chs"][pick]["loc"][9] for pick in picks], float) | ||
# n_wavelengths = len(set(unique_freqs)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cruft?
# freqs = np.array([raw.info["chs"][pick]["loc"][9] for pick in picks], float) | |
# n_wavelengths = len(set(unique_freqs)) |
# For multiple wavelengths: calculate all pairwise correlations within each group | ||
# and use the minimum as the quality metric |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why you need a conditional here... if n_wavelengths==2
then "all pairwise correlations" should be a single correlation and give an equivalent output to the branch above... so we shouldn't need it
or (ch1_value != pair_vals[0]) | ||
or (ch2_value != pair_vals[1]) | ||
# Validate channel grouping (same source-detector pairs, all pair_vals match) | ||
for i in range(0, len(picks), len(pair_vals)): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We prefer ii
or pi
to i
, more clearly different from 1j
and readable etc.
Looks like style CIs are unhappy -- until they are, most tests won't run |
Reference issue (if any)
Started discussion in #13401. Most of the description is copied from there with changes.
The same idea has also been raised in issue #9816, but that discussion did not result in a PR. 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 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.
I'd like to discuss implementation details and to explore devs' opinion on how feasible such changes would be. For this reason, this a draft PR, and some important points (like tests) haven't been addressed (at all) yet.
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).
Major changes
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.Minor changes
List of affected files and functions:
class RawSNIRF(BaseRaw)
has a simple check for 2 wavelengths and an error message.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._load_absorption()
is hard-coded to 2 wavelengths.scalp_coupling_index()
needs an algorithmic update_check_channels_ordered()
has various validation checks and_fnirs_spread_bads()
needs to spread bad markings over all wavelengths in groupI'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. Current tests for n=2 should also work.
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's a choice that should be discussed.
Additional context
I'll open another PR for the
mne-nirs
project once this PR has advanced a bit. In particular, the scalp coupling algorithm should be agreed on, since that project has a very similar, windowed implementation of it that probably should be updated with the changes here in mind.As I mentioned above, the current code is untested, as in I haven't actually run it with any .snirf files yet. I'm sorry for this, I'll try to do some testing as soon as possible.