Skip to content

Commit bca2f12

Browse files
committed
FIX: Use nominal wavelengths in BLL and SCI calculations
FIX: mne/io/hitachi tests with actual wavelength data now succeed * Beer-Lambert Law (BLL) and Scalp Coupling Index (SCI) calculations used the info structure to determine the number of wavelengths, but that lead to errors as the info structure can contain arbitrary data (e.g. different wavelengths for each channel). * BLL now uses the nominal wavelengths for the BLL calculation for all channels. Previously, it used the actual wavelengths of the first channel pair for all channels, which was incorrect if the channels have different actual wavelengths. * In the future, there may be an option in BLL to use the actual freq values for each channel, to improve calculation accuracy. * The Hitachi tests have data with actual wavelengths for each channel stored in the info structure. This caused issues with counting the number of wavelengths, which caused tests to fail.
1 parent 926b064 commit bca2f12

File tree

4 files changed

+23
-14
lines changed

4 files changed

+23
-14
lines changed

mne/preprocessing/nirs/_beer_lambert_law.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from ..._fiff.constants import FIFF
1212
from ...io import BaseRaw
1313
from ...utils import _validate_type, pinv, warn
14-
from ..nirs import _validate_nirs_info, source_detector_distances
14+
from ..nirs import _channel_frequencies, _validate_nirs_info, source_detector_distances
1515

1616

1717
def beer_lambert_law(raw, ppf=6.0):
@@ -37,8 +37,20 @@ def beer_lambert_law(raw, ppf=6.0):
3737
_validate_type(ppf, ("numeric", "array-like"), "ppf")
3838
ppf = np.array(ppf, float)
3939
picks = _validate_nirs_info(raw.info, fnirs="od", which="Beer-lambert")
40-
# This is the one place we *really* need the actual/accurate frequencies
41-
freqs = np.array([raw.info["chs"][pick]["loc"][9] for pick in picks], float)
40+
41+
# Use nominal channel frequencies
42+
#
43+
# Notes on implementation:
44+
# 1. Frequencies are calculated the same way as in nirs._validate_nirs_info().
45+
# 2. Wavelength values in the info structure may contain actual frequencies,
46+
# which may be used for more accurate calculation in the future.
47+
# 3. nirs._channel_frequencies uses both cw_amplitude and OD data to determine
48+
# frequencies, whereas we only need those from OD here. Is there any chance
49+
# that they're different?
50+
# 4. If actual frequencies were used, using np.unique() like below will lead to
51+
# errors. Instead, absorption coefficients will need to be calculated for
52+
# each individual frequency.
53+
freqs = _channel_frequencies(raw.info)
4254

4355
# Get unique wavelengths and determine number of wavelengths
4456
unique_freqs = np.unique(freqs)

mne/preprocessing/nirs/_scalp_coupling_index.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from ...io import BaseRaw
88
from ...utils import _validate_type, verbose
9-
from ..nirs import _validate_nirs_info
9+
from ..nirs import _channel_frequencies, _validate_nirs_info
1010

1111

1212
@verbose
@@ -57,8 +57,9 @@ def scalp_coupling_index(
5757
).get_data()
5858

5959
# Determine number of wavelengths per source-detector pair
60-
ch_wavelengths = [c["loc"][9] for c in raw.info["chs"]]
61-
n_wavelengths = len(set(ch_wavelengths))
60+
# We use nominal wavelengths as the info structure may contain arbitrary data.
61+
freqs = _channel_frequencies(raw.info)
62+
n_wavelengths = len(np.unique(freqs))
6263

6364
# freqs = np.array([raw.info["chs"][pick]["loc"][9] for pick in picks], float)
6465
# n_wavelengths = len(set(unique_freqs))
@@ -67,6 +68,7 @@ def scalp_coupling_index(
6768

6869
# Calculate all pairwise correlations within each group and use the minimum as SCI
6970
pair_indices = np.triu_indices(n_wavelengths, k=1)
71+
7072
for gg in range(0, len(picks), n_wavelengths):
7173
group_data = filtered_data[gg : gg + n_wavelengths]
7274

mne/preprocessing/nirs/nirs.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ def _check_channels_ordered(info, pair_vals, *, throw_errors=True, check_bads=Tr
155155
)
156156

157157
# Ensure wavelength info exists for waveform data
158+
# Note: currently, the only requirement for the wavelength field in info is
159+
# that it cannot be NaN. It depends on the data readers what is stored in it.
158160
if len(picks_wave) > 0:
159161
all_freqs = [info["chs"][ii]["loc"][9] for ii in picks_wave]
160162
# test for nan values first as those mess up the output of set()
@@ -164,13 +166,6 @@ def _check_channels_ordered(info, pair_vals, *, throw_errors=True, check_bads=Tr
164166
f'info["chs"] structure. The encoded wavelengths are {all_freqs}.',
165167
throw_errors,
166168
)
167-
# test if the info structure has the same number of freqs as pair_vals
168-
if len(pair_vals) != len(set(all_freqs)):
169-
picks = _throw_or_return_empty(
170-
f"The {error_word} in info must match the number of wavelengths, "
171-
f"but the data contains {len(set(all_freqs))} wavelengths instead.",
172-
throw_errors,
173-
)
174169

175170
# Validate the channel naming scheme
176171
for pick in picks:

mne/preprocessing/nirs/tests/test_beer_lambert_law.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def test_beer_lambert_unordered_errors():
7373
}
7474
)
7575
assert raw_od.ch_names[0] == "S1_D1 770" and raw_od.ch_names[1] == "S1_D1 840"
76-
with pytest.raises(ValueError, match="must match the number of wavelengths"):
76+
with pytest.raises(ValueError, match="NIRS channels not ordered correctly."):
7777
beer_lambert_law(raw_od)
7878

7979

0 commit comments

Comments
 (0)