diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index e3064a2b5e4..4578d4123e2 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -25,6 +25,8 @@ Enhancements ~~~~~~~~~~~~ - Add support for ahdr files in :func:`mne.io.read_raw_brainvision` (:gh:`10515` by :newcontrib:`Alessandro Tonin`) +- Add support for reading data from Gowerlabs devices to :func:`mne.io.read_raw_snirf` (:gh:`10555` by :newcontrib:`Samuel Powell` and `Robert Luke`_) + - Add support for ``overview_mode`` in :meth:`raw.plot() ` and related functions/methods (:gh:`10501` by `Eric Larson`_) - Add :meth:`mne.io.Raw.crop_by_annotations` method to get chunks of Raw data based on :class:`mne.Annotations`. (:gh:`10460` by `Alex Gramfort`_) @@ -82,3 +84,5 @@ Bugs API and behavior changes ~~~~~~~~~~~~~~~~~~~~~~~~ - When creating BEM surfaces via :func:`mne.bem.make_watershed_bem` and :func:`mne.bem.make_flash_bem`, the ``copy`` parameter now defaults to ``True``. This means that instead of creating symbolic links inside the FreeSurfer subject's ``bem`` folder, we now create "actual" files. This should avoid troubles when sharing files across different operating systems and file systems (:gh:`10531` by `Richard Höchenberger`_) + +- The ordering of channels returned by :func:`mne.io.read_raw_nirx` is now ordered by channel name, rather than the order provided by the manufacturer. This enables consistent ordering of channels across different file types (:gh:`10555` by `Robert Luke`_) diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 4a03c7d8c0b..b90a9673e0f 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -443,3 +443,5 @@ .. _Matthias Dold: https://matthiasdold.de .. _T. Wang: https://github.com/twang5 + +.. _Samuel Powell: https://github.com/samuelpowell diff --git a/mne/channels/tests/test_interpolation.py b/mne/channels/tests/test_interpolation.py index 37da056d260..8ca35ad3326 100644 --- a/mne/channels/tests/test_interpolation.py +++ b/mne/channels/tests/test_interpolation.py @@ -302,6 +302,6 @@ def test_interpolation_nirs(): assert bad_0_std_pre_interp > np.std(raw_od._data[bad_0]) raw_haemo = beer_lambert_law(raw_od, ppf=6) raw_haemo.info['bads'] = raw_haemo.ch_names[2:4] - assert raw_haemo.info['bads'] == ['S1_D2 hbo', 'S1_D2 hbr'] + assert raw_haemo.info['bads'] == ['S10_D11 hbo', 'S10_D11 hbr'] raw_haemo.interpolate_bads() assert raw_haemo.info['bads'] == [] diff --git a/mne/datasets/config.py b/mne/datasets/config.py index b9132f4fb11..d4a35fbd594 100644 --- a/mne/datasets/config.py +++ b/mne/datasets/config.py @@ -87,7 +87,7 @@ # respective repos, and make a new release of the dataset on GitHub. Then # update the checksum in the MNE_DATASETS dict below, and change version # here: ↓↓↓↓↓ ↓↓↓ -RELEASES = dict(testing='0.134', misc='0.23') +RELEASES = dict(testing='0.135', misc='0.23') TESTING_VERSIONED = f'mne-testing-data-{RELEASES["testing"]}' MISC_VERSIONED = f'mne-misc-data-{RELEASES["misc"]}' @@ -111,7 +111,7 @@ # Testing and misc are at the top as they're updated most often MNE_DATASETS['testing'] = dict( archive_name=f'{TESTING_VERSIONED}.tar.gz', # 'mne-testing-data', - hash='md5:9d234ff0156e6feccbbbc01469a861eb', + hash='md5:db40791e786fa776e8717cf50b43b0ec', url=('https://codeload.github.com/mne-tools/mne-testing-data/' f'tar.gz/{RELEASES["testing"]}'), folder_name='MNE-testing-data', diff --git a/mne/io/nirx/nirx.py b/mne/io/nirx/nirx.py index 8b1a45f3312..c025f2b8942 100644 --- a/mne/io/nirx/nirx.py +++ b/mne/io/nirx/nirx.py @@ -459,6 +459,9 @@ def __init__(self, fname, saturated, preload=False, verbose=None): annot = Annotations(onset, duration, description, ch_names=ch_names) self.set_annotations(annot) + sort_idx = np.argsort(self.ch_names) + self.pick(picks=sort_idx) + def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): """Read a segment of data from a file. diff --git a/mne/io/nirx/tests/test_nirx.py b/mne/io/nirx/tests/test_nirx.py index 3e869795c60..73172bd4184 100644 --- a/mne/io/nirx/tests/test_nirx.py +++ b/mne/io/nirx/tests/test_nirx.py @@ -95,11 +95,12 @@ def test_nirsport_v2(): # nirsite https://github.com/mne-tools/mne-testing-data/pull/86 # figure 3 allowed_distance_error = 0.005 - distances = source_detector_distances(raw.info) - assert_allclose(distances[::2][:14], - [0.0304, 0.0411, 0.008, 0.0400, 0.008, 0.0310, 0.0411, - 0.008, 0.0299, 0.008, 0.0370, 0.008, 0.0404, 0.008], - atol=allowed_distance_error) + assert_allclose(source_detector_distances(raw.copy(). + pick("S1_D1 760").info), + [0.0304], atol=allowed_distance_error) + assert_allclose(source_detector_distances(raw.copy(). + pick("S2_D2 760").info), + [0.0400], atol=allowed_distance_error) # Test location of detectors # The locations of detectors can be seen in the first @@ -118,9 +119,9 @@ def test_nirsport_v2(): assert_allclose( mni_locs[2], [-0.0841, -0.0138, 0.0248], atol=allowed_dist_error) - assert raw.info['ch_names'][34][3:5] == 'D5' + assert raw.info['ch_names'][13][3:5] == 'D5' assert_allclose( - mni_locs[34], [0.0845, -0.0451, -0.0123], atol=allowed_dist_error) + mni_locs[13], [0.0845, -0.0451, -0.0123], atol=allowed_dist_error) # Test location of sensors # The locations of sensors can be seen in the second @@ -138,9 +139,9 @@ def test_nirsport_v2(): assert_allclose( mni_locs[9], [-0.0, -0.1195, 0.0142], atol=allowed_dist_error) - assert raw.info['ch_names'][34][:2] == 'S8' + assert raw.info['ch_names'][39][:2] == 'S8' assert_allclose( - mni_locs[34], [0.0828, -0.046, 0.0285], atol=allowed_dist_error) + mni_locs[39], [0.0828, -0.046, 0.0285], atol=allowed_dist_error) assert len(raw.annotations) == 3 assert raw.annotations.description[0] == '1.0' @@ -290,7 +291,7 @@ def test_nirx_15_2_short(): # Test channel naming assert raw.info['ch_names'][:4] == ["S1_D1 760", "S1_D1 850", "S1_D9 760", "S1_D9 850"] - assert raw.info['ch_names'][24:26] == ["S5_D13 760", "S5_D13 850"] + assert raw.info['ch_names'][24:26] == ["S5_D8 760", "S5_D8 850"] # Test frequency encoding assert raw.info['chs'][0]['loc'][9] == 760 @@ -307,17 +308,18 @@ def test_nirx_15_2_short(): # nirsite https://github.com/mne-tools/mne-testing-data/pull/51 # step 4 figure 2 allowed_distance_error = 0.0002 - distances = source_detector_distances(raw.info) - assert_allclose(distances[::2], [ - 0.0304, 0.0078, 0.0310, 0.0086, 0.0416, - 0.0072, 0.0389, 0.0075, 0.0558, 0.0562, - 0.0561, 0.0565, 0.0077], atol=allowed_distance_error) + assert_allclose(source_detector_distances(raw.copy(). + pick("S1_D1 760").info), + [0.0304], atol=allowed_distance_error) + assert_allclose(source_detector_distances(raw.copy(). + pick("S2_D10 760").info), + [0.0086], atol=allowed_distance_error) # Test which channels are short # These are the ones marked as red at # https://github.com/mne-tools/mne-testing-data/pull/51 step 4 figure 2 is_short = short_channels(raw.info) - assert_array_equal(is_short[:9:2], [False, True, False, True, False]) + assert_array_equal(is_short[:9:2], [False, True, True, False, True]) is_short = short_channels(raw.info, threshold=0.003) assert_array_equal(is_short[:3:2], [False, False]) is_short = short_channels(raw.info, threshold=50) @@ -344,29 +346,29 @@ def test_nirx_15_2_short(): assert_allclose( mni_locs[0], [-0.0841, -0.0464, -0.0129], atol=allowed_dist_error) - assert raw.info['ch_names'][4][3:5] == 'D3' + assert raw.info['ch_names'][6][3:5] == 'D3' assert_allclose( - mni_locs[4], [0.0846, -0.0142, -0.0156], atol=allowed_dist_error) + mni_locs[6], [0.0846, -0.0142, -0.0156], atol=allowed_dist_error) - assert raw.info['ch_names'][8][3:5] == 'D2' + assert raw.info['ch_names'][10][3:5] == 'D2' assert_allclose( - mni_locs[8], [0.0207, -0.1062, 0.0484], atol=allowed_dist_error) + mni_locs[10], [0.0207, -0.1062, 0.0484], atol=allowed_dist_error) - assert raw.info['ch_names'][12][3:5] == 'D4' + assert raw.info['ch_names'][14][3:5] == 'D4' assert_allclose( - mni_locs[12], [-0.0196, 0.0821, 0.0275], atol=allowed_dist_error) + mni_locs[14], [-0.0196, 0.0821, 0.0275], atol=allowed_dist_error) - assert raw.info['ch_names'][16][3:5] == 'D5' + assert raw.info['ch_names'][18][3:5] == 'D5' assert_allclose( - mni_locs[16], [-0.0360, 0.0276, 0.0778], atol=allowed_dist_error) + mni_locs[18], [-0.0360, 0.0276, 0.0778], atol=allowed_dist_error) - assert raw.info['ch_names'][19][3:5] == 'D6' + assert raw.info['ch_names'][20][3:5] == 'D6' assert_allclose( - mni_locs[19], [0.0352, 0.0283, 0.0780], atol=allowed_dist_error) + mni_locs[20], [0.0352, 0.0283, 0.0780], atol=allowed_dist_error) - assert raw.info['ch_names'][21][3:5] == 'D7' + assert raw.info['ch_names'][23][3:5] == 'D7' assert_allclose( - mni_locs[21], [0.0388, -0.0477, 0.0932], atol=allowed_dist_error) + mni_locs[23], [0.0388, -0.0477, 0.0932], atol=allowed_dist_error) @requires_testing_data @@ -381,7 +383,7 @@ def test_nirx_15_3_short(): # Test channel naming assert raw.info['ch_names'][:4] == ["S1_D2 760", "S1_D2 850", "S1_D9 760", "S1_D9 850"] - assert raw.info['ch_names'][24:26] == ["S5_D13 760", "S5_D13 850"] + assert raw.info['ch_names'][24:26] == ["S5_D8 760", "S5_D8 850"] # Test frequency encoding assert raw.info['chs'][0]['loc'][9] == 760 @@ -398,17 +400,18 @@ def test_nirx_15_3_short(): # Test distance between optodes matches values from # https://github.com/mne-tools/mne-testing-data/pull/72 allowed_distance_error = 0.001 - distances = source_detector_distances(raw.info) - assert_allclose(distances[::2], [ - 0.0304, 0.0078, 0.0310, 0.0086, 0.0416, - 0.0072, 0.0389, 0.0075, 0.0558, 0.0562, - 0.0561, 0.0565, 0.0077], atol=allowed_distance_error) + assert_allclose(source_detector_distances(raw.copy(). + pick("S1_D2 760").info), + [0.0304], atol=allowed_distance_error) + assert_allclose(source_detector_distances(raw.copy(). + pick("S5_D13 760").info), + [0.0076], atol=allowed_distance_error) # Test which channels are short # These are the ones marked as red at # https://github.com/mne-tools/mne-testing-data/pull/72 is_short = short_channels(raw.info) - assert_array_equal(is_short[:9:2], [False, True, False, True, False]) + assert_array_equal(is_short[:9:2], [False, True, False, True, True]) is_short = short_channels(raw.info, threshold=0.003) assert_array_equal(is_short[:3:2], [False, False]) is_short = short_channels(raw.info, threshold=50) @@ -435,25 +438,25 @@ def test_nirx_15_3_short(): assert_allclose( mni_locs[4], [0.0846, -0.0142, -0.0156], atol=allowed_dist_error) - assert raw.info['ch_names'][8][3:5] == 'D3' + assert raw.info['ch_names'][10][3:5] == 'D3' assert_allclose( - mni_locs[8], [0.0207, -0.1062, 0.0484], atol=allowed_dist_error) + mni_locs[10], [0.0207, -0.1062, 0.0484], atol=allowed_dist_error) - assert raw.info['ch_names'][12][3:5] == 'D4' + assert raw.info['ch_names'][14][3:5] == 'D4' assert_allclose( - mni_locs[12], [-0.0196, 0.0821, 0.0275], atol=allowed_dist_error) + mni_locs[14], [-0.0196, 0.0821, 0.0275], atol=allowed_dist_error) - assert raw.info['ch_names'][16][3:5] == 'D5' + assert raw.info['ch_names'][18][3:5] == 'D5' assert_allclose( - mni_locs[16], [-0.0360, 0.0276, 0.0778], atol=allowed_dist_error) + mni_locs[18], [-0.0360, 0.0276, 0.0778], atol=allowed_dist_error) - assert raw.info['ch_names'][19][3:5] == 'D6' + assert raw.info['ch_names'][20][3:5] == 'D6' assert_allclose( - mni_locs[19], [0.0388, -0.0477, 0.0932], atol=allowed_dist_error) + mni_locs[20], [0.0388, -0.0477, 0.0932], atol=allowed_dist_error) - assert raw.info['ch_names'][21][3:5] == 'D7' + assert raw.info['ch_names'][22][3:5] == 'D7' assert_allclose( - mni_locs[21], [-0.0394, -0.0483, 0.0928], atol=allowed_dist_error) + mni_locs[22], [-0.0394, -0.0483, 0.0928], atol=allowed_dist_error) @requires_testing_data @@ -501,8 +504,8 @@ def test_nirx_15_2(): tzinfo=dt.timezone.utc) # Test channel naming - assert raw.info['ch_names'][:4] == ["S1_D1 760", "S1_D1 850", - "S1_D10 760", "S1_D10 850"] + assert raw.info['ch_names'][:4] == ["S10_D10 760", "S10_D10 850", + "S10_D9 760", "S10_D9 850"] # Test info import assert raw.info['subject_info'] == dict(sex=1, first_name="TestRecording", @@ -519,13 +522,13 @@ def test_nirx_15_2(): head_mri_t, _ = _get_trans('fsaverage', 'head', 'mri') mni_locs = apply_trans(head_mri_t, locs) - assert raw.info['ch_names'][0][3:5] == 'D1' + assert raw.info['ch_names'][28][3:5] == 'D1' assert_allclose( - mni_locs[0], [-0.0292, 0.0852, -0.0142], atol=allowed_dist_error) + mni_locs[28], [-0.0292, 0.0852, -0.0142], atol=allowed_dist_error) - assert raw.info['ch_names'][15][3:5] == 'D4' + assert raw.info['ch_names'][42][3:5] == 'D4' assert_allclose( - mni_locs[15], [-0.0739, -0.0756, -0.0075], atol=allowed_dist_error) + mni_locs[42], [-0.0739, -0.0756, -0.0075], atol=allowed_dist_error) # Old name aliases for backward compat assert 'fnirs_cw_amplitude' in raw @@ -549,12 +552,12 @@ def test_nirx_15_0(): tzinfo=dt.timezone.utc) # Test channel naming - assert raw.info['ch_names'][:12] == ["S1_D1 760", "S1_D1 850", + assert raw.info['ch_names'][:12] == ["S10_D10 760", "S10_D10 850", + "S1_D1 760", "S1_D1 850", "S2_D2 760", "S2_D2 850", "S3_D3 760", "S3_D3 850", "S4_D4 760", "S4_D4 850", - "S5_D5 760", "S5_D5 850", - "S6_D6 760", "S6_D6 850"] + "S5_D5 760", "S5_D5 850"] # Test info import assert raw.info['subject_info'] == {'birthday': (2004, 10, 27), @@ -572,20 +575,23 @@ def test_nirx_15_0(): head_mri_t, _ = _get_trans('fsaverage', 'head', 'mri') mni_locs = apply_trans(head_mri_t, locs) - assert raw.info['ch_names'][0][3:5] == 'D1' + assert raw.info['ch_names'][2][3:5] == 'D1' assert_allclose( - mni_locs[0], [0.0287, -0.1143, -0.0332], atol=allowed_dist_error) + mni_locs[2], [0.0287, -0.1143, -0.0332], atol=allowed_dist_error) - assert raw.info['ch_names'][15][3:5] == 'D8' + assert raw.info['ch_names'][17][3:5] == 'D8' assert_allclose( - mni_locs[15], [-0.0693, -0.0480, 0.0657], atol=allowed_dist_error) + mni_locs[17], [-0.0693, -0.0480, 0.0657], atol=allowed_dist_error) # Test distance between optodes matches values from allowed_distance_error = 0.0002 - distances = source_detector_distances(raw.info) - assert_allclose(distances[::2], [ - 0.0301, 0.0315, 0.0343, 0.0368, 0.0408, - 0.0399, 0.0393, 0.0367, 0.0336, 0.0447], atol=allowed_distance_error) + + assert_allclose(source_detector_distances(raw.copy(). + pick("S1_D1 760").info), + [0.0300], atol=allowed_distance_error) + assert_allclose(source_detector_distances(raw.copy(). + pick("S7_D7 760").info), + [0.0392], atol=allowed_distance_error) @requires_testing_data diff --git a/mne/io/snirf/_snirf.py b/mne/io/snirf/_snirf.py index 5d28835b29a..b5763d69fbd 100644 --- a/mne/io/snirf/_snirf.py +++ b/mne/io/snirf/_snirf.py @@ -96,6 +96,10 @@ def __init__(self, fname, optode_frame="unknown", "MNE does not support this feature. " "Only the first dataset will be processed.") + manafacturer = _get_metadata_str(dat, "ManufacturerName") + if (optode_frame == "unknown") & (manafacturer == "Gowerlabs"): + optode_frame = "head" + snirf_data_type = np.array(dat.get('nirs/data1/measurementList1' '/dataType')).item() if snirf_data_type not in [1, 99999]: @@ -109,20 +113,8 @@ def __init__(self, fname, optode_frame="unknown", last_samps = dat.get('/nirs/data1/dataTimeSeries').shape[0] - 1 - samplingrate_raw = np.array(dat.get('nirs/data1/time')) - sampling_rate = 0 - if samplingrate_raw.shape == (2, 1): - # specified as onset/samplerate - warn("Onset/sample rate SNIRF not yet supported.") - else: - # specified as time points - fs_diff = np.around(np.diff(samplingrate_raw), decimals=4) - if len(np.unique(fs_diff)) == 1: - # Uniformly sampled data - sampling_rate = 1. / np.unique(fs_diff) - else: - # print(np.unique(fs_diff)) - warn("Non uniform sampled data not supported.") + sampling_rate = _extract_sampling_rate(dat) + if sampling_rate == 0: warn("Unable to extract sample rate from SNIRF file.") @@ -280,16 +272,11 @@ def natural_keys(text): # Update info info.update(subject_info=subject_info) - LengthUnit = np.array(dat.get('/nirs/metaDataTags/LengthUnit')) - LengthUnit = _correct_shape(LengthUnit)[0].decode('UTF-8') - scal = 1 - if "cm" in LengthUnit: - scal = 100 - elif "mm" in LengthUnit: - scal = 1000 + length_unit = _get_metadata_str(dat, "LengthUnit") + length_scaling = _get_lengthunit_scaling(length_unit) - srcPos3D /= scal - detPos3D /= scal + srcPos3D /= length_scaling + detPos3D /= length_scaling if optode_frame in ["mri", "meg"]: # These are all in MNI or MEG coordinates, so let's transform @@ -331,15 +318,17 @@ def natural_keys(text): if 'landmarkPos3D' in dat.get('nirs/probe/'): diglocs = np.array(dat.get('/nirs/probe/landmarkPos3D')) + diglocs /= length_scaling digname = np.array(dat.get('/nirs/probe/landmarkLabels')) nasion, lpa, rpa, hpi = None, None, None, None extra_ps = dict() for idx, dign in enumerate(digname): - if dign == b'LPA': + dign = dign.lower() + if dign in [b'lpa', b'al']: lpa = diglocs[idx, :3] - elif dign == b'NASION': + elif dign in [b'nasion']: nasion = diglocs[idx, :3] - elif dign == b'RPA': + elif dign in [b'rpa', b'ar']: rpa = diglocs[idx, :3] else: extra_ps[f'EEG{len(extra_ps) + 1:03d}'] = \ @@ -420,14 +409,10 @@ def natural_keys(text): annot.append(data[:, 0], 1.0, desc.decode('UTF-8')) self.set_annotations(annot, emit_warning=False) - # Reorder channels to match expected ordering in MNE if required' + # MNE requires channels are paired as alternating wavelengths if len(_validate_nirs_info(self.info, throw_errors=False)) == 0: - num_chans = len(self.ch_names) - chans = [] - for idx in range(num_chans // 2): - chans.append(idx) - chans.append(idx + num_chans // 2) - self.pick(picks=chans) + sort_idx = np.argsort(self.ch_names) + self.pick(picks=sort_idx) # Validate that the fNIRS info is correctly formatted _validate_nirs_info(self.info) @@ -447,3 +432,58 @@ def _correct_shape(arr): if arr.shape == (): arr = arr[np.newaxis] return arr + + +def _get_timeunit_scaling(time_unit): + """MNE expects time in seconds, return required scaling.""" + scalings = {'ms': 1000, 's': 1, 'unknown': 1} + if time_unit in scalings: + return scalings[time_unit] + else: + raise RuntimeError(f'The time unit {time_unit} is not supported by ' + 'MNE. Please report this error as a GitHub' + 'issue to inform the developers.') + + +def _get_lengthunit_scaling(length_unit): + """MNE expects distance in m, return required scaling.""" + scalings = {'cm': 100, 'mm': 1000} + if length_unit in scalings: + return scalings[length_unit] + else: + raise RuntimeError(f'The time unit {length_unit} is not supported by ' + 'MNE. Please report this error as a GitHub' + 'issue to inform the developers.') + + +def _extract_sampling_rate(dat): + """Extract the sample rate from the time field.""" + time_data = np.array(dat.get('nirs/data1/time')) + sampling_rate = 0 + if len(time_data) == 2: + # specified as onset, samplerate + sampling_rate = 1. / (time_data[1] - time_data[0]) + else: + # specified as time points + fs_diff = np.around(np.diff(time_data), decimals=4) + if len(np.unique(fs_diff)) == 1: + # Uniformly sampled data + sampling_rate = 1. / np.unique(fs_diff) + else: + warn("MNE does not currently support reading " + "SNIRF files with non-uniform sampled data.") + + time_unit = _get_metadata_str(dat, "TimeUnit") + time_unit_scaling = _get_timeunit_scaling(time_unit) + sampling_rate *= time_unit_scaling + + return sampling_rate + + +def _get_metadata_str(dat, field): + if field not in np.array(dat.get('nirs/metaDataTags')): + return None + data = dat.get(f'/nirs/metaDataTags/{field}') + data = _correct_shape(np.array(data)) + data = str(data[0], 'utf-8') + return data diff --git a/mne/io/snirf/tests/test_snirf.py b/mne/io/snirf/tests/test_snirf.py index 356e98cdfdf..9709976f198 100644 --- a/mne/io/snirf/tests/test_snirf.py +++ b/mne/io/snirf/tests/test_snirf.py @@ -50,16 +50,25 @@ ft_od = op.join(testing_path, 'SNIRF', 'FieldTrip', '220307_opticaldensity.snirf') +# GowerLabs +lumo110 = op.join(testing_path, 'SNIRF', 'GowerLabs', 'lumomat-1-1-0.snirf') + + +def _get_loc(raw, ch_name): + return raw.copy().pick(ch_name).info['chs'][0]['loc'] + @requires_testing_data @pytest.mark.filterwarnings('ignore:.*contains 2D location.*:') +@pytest.mark.filterwarnings('ignore:.*measurement date.*:') @pytest.mark.parametrize('fname', ([sfnirs_homer_103_wShort, nirx_nirsport2_103, sfnirs_homer_103_153, nirx_nirsport2_103, nirx_nirsport2_103_2, nirx_nirsport2_103_2, - kernel_hb + kernel_hb, + lumo110 ])) def test_basic_reading_and_min_process(fname): """Test reading SNIRF files and minimum typical processing.""" @@ -73,6 +82,18 @@ def test_basic_reading_and_min_process(fname): assert 'hbr' in raw +@requires_testing_data +@pytest.mark.filterwarnings('ignore:.*measurement date.*:') +def test_snirf_gowerlabs(): + """Test reading SNIRF files.""" + raw = read_raw_snirf(lumo110, preload=True) + + assert raw._data.shape == (216, 274) + assert raw.info['dig'][0]['coord_frame'] == FIFF.FIFFV_COORD_HEAD + assert len(raw.ch_names) == 216 + assert_allclose(raw.info['sfreq'], 10.0) + + @requires_testing_data def test_snirf_basic(): """Test reading SNIRF files.""" @@ -85,7 +106,6 @@ def test_snirf_basic(): # Test channel naming assert raw.info['ch_names'][:4] == ["S1_D1 760", "S1_D1 850", "S1_D9 760", "S1_D9 850"] - assert raw.info['ch_names'][24:26] == ["S5_D13 760", "S5_D13 850"] # Test frequency encoding assert raw.info['chs'][0]['loc'][9] == 760 @@ -93,25 +113,25 @@ def test_snirf_basic(): # Test source locations assert_allclose([-8.6765 * 1e-2, 0.0049 * 1e-2, -2.6167 * 1e-2], - raw.info['chs'][0]['loc'][3:6], rtol=0.02) + _get_loc(raw, 'S1_D1 760')[3:6], rtol=0.02) assert_allclose([7.9579 * 1e-2, -2.7571 * 1e-2, -2.2631 * 1e-2], - raw.info['chs'][4]['loc'][3:6], rtol=0.02) + _get_loc(raw, 'S2_D3 760')[3:6], rtol=0.02) assert_allclose([-2.1387 * 1e-2, -8.8874 * 1e-2, 3.8393 * 1e-2], - raw.info['chs'][8]['loc'][3:6], rtol=0.02) + _get_loc(raw, 'S3_D2 760')[3:6], rtol=0.02) assert_allclose([1.8602 * 1e-2, 9.7164 * 1e-2, 1.7539 * 1e-2], - raw.info['chs'][12]['loc'][3:6], rtol=0.02) + _get_loc(raw, 'S4_D4 760')[3:6], rtol=0.02) assert_allclose([-0.1108 * 1e-2, 0.7066 * 1e-2, 8.9883 * 1e-2], - raw.info['chs'][16]['loc'][3:6], rtol=0.02) + _get_loc(raw, 'S5_D5 760')[3:6], rtol=0.02) # Test detector locations assert_allclose([-8.0409 * 1e-2, -2.9677 * 1e-2, -2.5415 * 1e-2], - raw.info['chs'][0]['loc'][6:9], rtol=0.02) + _get_loc(raw, 'S1_D1 760')[6:9], rtol=0.02) assert_allclose([-8.7329 * 1e-2, 0.7577 * 1e-2, -2.7980 * 1e-2], - raw.info['chs'][3]['loc'][6:9], rtol=0.02) + _get_loc(raw, 'S1_D9 850')[6:9], rtol=0.02) assert_allclose([9.2027 * 1e-2, 0.0161 * 1e-2, -2.8909 * 1e-2], - raw.info['chs'][5]['loc'][6:9], rtol=0.02) + _get_loc(raw, 'S2_D3 850')[6:9], rtol=0.02) assert_allclose([7.7548 * 1e-2, -3.5901 * 1e-2, -2.3179 * 1e-2], - raw.info['chs'][7]['loc'][6:9], rtol=0.02) + _get_loc(raw, 'S2_D10 850')[6:9], rtol=0.02) assert 'fnirs_cw_amplitude' in raw @@ -186,9 +206,9 @@ def test_snirf_nirsport2(): assert_almost_equal(raw.info['sfreq'], 7.6, decimal=1) # Test channel naming - assert raw.info['ch_names'][:4] == ['S1_D1 760', 'S1_D1 850', - 'S1_D3 760', 'S1_D3 850'] - assert raw.info['ch_names'][24:26] == ['S6_D4 760', 'S6_D4 850'] + assert raw.info['ch_names'][:4] == ['S10_D3 760', 'S10_D3 850', + 'S10_D9 760', 'S10_D9 850'] + assert raw.info['ch_names'][24:26] == ['S15_D11 760', 'S15_D11 850'] # Test frequency encoding assert raw.info['chs'][0]['loc'][9] == 760 @@ -226,7 +246,7 @@ def test_snirf_nirsport2_w_positions(): # Test channel naming assert raw.info['ch_names'][:4] == ['S1_D1 760', 'S1_D1 850', 'S1_D6 760', 'S1_D6 850'] - assert raw.info['ch_names'][24:26] == ['S6_D4 760', 'S6_D4 850'] + assert raw.info['ch_names'][24:26] == ['S6_D14 760', 'S6_D14 850'] # Test frequency encoding assert raw.info['chs'][0]['loc'][9] == 760 @@ -238,11 +258,12 @@ def test_snirf_nirsport2_w_positions(): # nirsite https://github.com/mne-tools/mne-testing-data/pull/86 # figure 3 allowed_distance_error = 0.005 - distances = source_detector_distances(raw.info) - assert_allclose(distances[::2][:14], - [0.0304, 0.0411, 0.008, 0.0400, 0.008, 0.0310, 0.0411, - 0.008, 0.0299, 0.008, 0.0370, 0.008, 0.0404, 0.008], - atol=allowed_distance_error) + assert_allclose(source_detector_distances(raw.copy(). + pick("S1_D1 760").info), + [0.0304], atol=allowed_distance_error) + assert_allclose(source_detector_distances(raw.copy(). + pick("S2_D2 760").info), + [0.0400], atol=allowed_distance_error) # Test location of detectors # The locations of detectors can be seen in the first @@ -261,10 +282,6 @@ def test_snirf_nirsport2_w_positions(): assert_allclose( mni_locs[2], [-0.0841, -0.0138, 0.0248], atol=allowed_dist_error) - assert raw.info['ch_names'][34][3:5] == 'D5' - assert_allclose( - mni_locs[34], [0.0845, -0.0451, -0.0123], atol=allowed_dist_error) - # Test location of sensors # The locations of sensors can be seen in the second # figure on this page... diff --git a/mne/preprocessing/nirs/tests/test_beer_lambert_law.py b/mne/preprocessing/nirs/tests/test_beer_lambert_law.py index c6fde250e77..48115e3adc1 100644 --- a/mne/preprocessing/nirs/tests/test_beer_lambert_law.py +++ b/mne/preprocessing/nirs/tests/test_beer_lambert_law.py @@ -88,12 +88,21 @@ def test_beer_lambert_v_matlab(): 'nirx_15_0_recording_bl.mat') matlab_data = read_mat(matlab_fname) + matlab_names = ["_"] * len(raw.ch_names) + for idx in range(len(raw.ch_names)): + matlab_names[idx] = ("S" + str(int(matlab_data['sources'][idx])) + + "_D" + str(int(matlab_data['detectors'][idx])) + + " " + matlab_data['type'][idx]) + matlab_to_mne = np.argsort(matlab_names) + for idx in range(raw.get_data().shape[0]): - mean_error = np.mean(matlab_data['data'][:, idx] - + matlab_idx = matlab_to_mne[idx] + + mean_error = np.mean(matlab_data['data'][:, matlab_idx] - raw._data[idx]) assert mean_error < 0.1 - matlab_name = ("S" + str(int(matlab_data['sources'][idx])) + - "_D" + str(int(matlab_data['detectors'][idx])) + - " " + matlab_data['type'][idx]) + matlab_name = ("S" + str(int(matlab_data['sources'][matlab_idx])) + + "_D" + str(int(matlab_data['detectors'][matlab_idx])) + + " " + matlab_data['type'][matlab_idx]) assert raw.info['ch_names'][idx] == matlab_name diff --git a/mne/preprocessing/nirs/tests/test_temporal_derivative_distribution_repair.py b/mne/preprocessing/nirs/tests/test_temporal_derivative_distribution_repair.py index 922a99dcb09..546eae9e2ab 100644 --- a/mne/preprocessing/nirs/tests/test_temporal_derivative_distribution_repair.py +++ b/mne/preprocessing/nirs/tests/test_temporal_derivative_distribution_repair.py @@ -28,7 +28,7 @@ def test_temporal_derivative_distribution_repair(fname, tmp_path): # Add a baseline shift artifact about half way through data max_shift = np.max(np.diff(raw._data[0])) shift_amp = 5 * max_shift - raw._data[0, 0:30] = raw._data[0, 0:30] - (shift_amp) + raw._data[0, 0:30] = raw._data[0, 0:30] - (1.1 * shift_amp) # make one channel zero std raw._data[1] = 0. raw._data[2] = 1. diff --git a/tutorials/io/30_reading_fnirs_data.py b/tutorials/io/30_reading_fnirs_data.py index 3215c26ea63..15f63e4a678 100644 --- a/tutorials/io/30_reading_fnirs_data.py +++ b/tutorials/io/30_reading_fnirs_data.py @@ -40,8 +40,8 @@ is designed by the fNIRS community in an effort to facilitate sharing and analysis of fNIRS data. And is the official format of the Society for functional near-infrared spectroscopy (SfNIRS). -The manufacturers NIRx, Kernel, and Cortivision export data in the SNIRF -format, and these files can be imported in to MNE. +The manufacturers Gowerlabs, NIRx, Kernel, and Cortivision +export data in the SNIRF format, and these files can be imported in to MNE. SNIRF is the preferred format for reading data in to MNE-Python. Data stored in the SNIRF format can be read in using :func:`mne.io.read_raw_snirf`. @@ -68,6 +68,7 @@ Kernel ICBM 2009b mri ======= ================== ================= +The coordinate system is automatically detected for Gowerlabs SNIRF files. ***********************