From 6f575951103d38cfed5cf45e1eb316660bd92ab1 Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Thu, 21 Apr 2022 18:57:40 +1000 Subject: [PATCH 01/13] Add support for SNIRF timeunit --- mne/io/snirf/_snirf.py | 52 ++++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/mne/io/snirf/_snirf.py b/mne/io/snirf/_snirf.py index 5d28835b29a..a6c5da51632 100644 --- a/mne/io/snirf/_snirf.py +++ b/mne/io/snirf/_snirf.py @@ -109,20 +109,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.") @@ -447,3 +435,39 @@ 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 scaling from time_unit to seconds.""" + 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 _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 = _correct_shape(np.array(dat.get('/nirs/metaDataTags/TimeUnit'))) + time_unit = str(time_unit[0], 'utf-8') + time_unit_scaling = _get_timeunit_scaling(time_unit) + sampling_rate *= time_unit_scaling + + return sampling_rate From 08fc60f761767f48a998d020592081057e70af0a Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Thu, 21 Apr 2022 19:05:30 +1000 Subject: [PATCH 02/13] Flake --- mne/io/snirf/_snirf.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mne/io/snirf/_snirf.py b/mne/io/snirf/_snirf.py index a6c5da51632..58781edfd4a 100644 --- a/mne/io/snirf/_snirf.py +++ b/mne/io/snirf/_snirf.py @@ -438,14 +438,14 @@ def _correct_shape(arr): def _get_timeunit_scaling(time_unit): - """MNE expects time in seconds, return scaling from time_unit to seconds.""" + """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.') + 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 _extract_sampling_rate(dat): @@ -465,7 +465,8 @@ def _extract_sampling_rate(dat): warn("MNE does not currently support reading " "SNIRF files with non-uniform sampled data.") - time_unit = _correct_shape(np.array(dat.get('/nirs/metaDataTags/TimeUnit'))) + time_unit = dat.get('/nirs/metaDataTags/TimeUnit') + time_unit = _correct_shape(np.array(time_unit)) time_unit = str(time_unit[0], 'utf-8') time_unit_scaling = _get_timeunit_scaling(time_unit) sampling_rate *= time_unit_scaling From f4fb27360cfd906dfc55f6278811c89bc000f0fa Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Sun, 1 May 2022 16:19:55 +1000 Subject: [PATCH 03/13] Fix landmark reading --- doc/changes/latest.inc | 2 + doc/changes/names.inc | 2 + mne/io/nirx/nirx.py | 3 ++ mne/io/snirf/_snirf.py | 44 +++++++++++++-------- mne/io/snirf/tests/test_snirf.py | 65 ++++++++++++++++++++------------ 5 files changed, 77 insertions(+), 39 deletions(-) diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index 2097b24a4d9..74ddc5498e2 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -27,6 +27,8 @@ Enhancements - The ``pick_channels`` method gained a ``verbose`` parameter, allowing e.g. to suppress messages about removed projectors (:gh:`10544` by `Richard Höchenberger`_) +- Add support for reading data from Gowerlabs devices to :func:`mne.io.read_raw_snirf` (:gh:`10544` by `Robert Luke`_ and `Samuel Powell`_) + Bugs ~~~~ - Fix bug in :func:`mne.io.read_raw_brainvision` when BrainVision data are acquired with the Brain Products "V-Amp" amplifier and disabled lowpass filter is marked with value ``0`` (:gh:`10517` by :newcontrib:`Alessandro Tonin`) diff --git a/doc/changes/names.inc b/doc/changes/names.inc index aebab1dc977..aa6f808bac2 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -441,3 +441,5 @@ .. _Nikolai Chapochnikov: https://github.com/chapochn .. _Matthias Dold: https://matthiasdold.de + +.. _Samuel Powell: https://github.com/samuelpowell 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/snirf/_snirf.py b/mne/io/snirf/_snirf.py index 58781edfd4a..2261f018e3c 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]: @@ -268,8 +272,7 @@ 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') + LengthUnit = _get_metadata_str(dat, "LengthUnit") scal = 1 if "cm" in LengthUnit: scal = 100 @@ -319,15 +322,17 @@ def natural_keys(text): if 'landmarkPos3D' in dat.get('nirs/probe/'): diglocs = np.array(dat.get('/nirs/probe/landmarkPos3D')) + diglocs /= scal 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}'] = \ @@ -408,14 +413,16 @@ 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) + # 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) @@ -465,10 +472,17 @@ def _extract_sampling_rate(dat): warn("MNE does not currently support reading " "SNIRF files with non-uniform sampled data.") - time_unit = dat.get('/nirs/metaDataTags/TimeUnit') - time_unit = _correct_shape(np.array(time_unit)) - time_unit = str(time_unit[0], 'utf-8') + 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..9ab06c937a3 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,17 @@ 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) + + # Test data import + assert raw._data.shape == (216, 274) + assert raw.info['dig'][0]['coord_frame'] == FIFF.FIFFV_COORD_HEAD + + @requires_testing_data def test_snirf_basic(): """Test reading SNIRF files.""" @@ -85,7 +105,7 @@ 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"] + # assert raw.info['ch_names'][24:26] == ["S5_D8 760", "S5_D8 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... From 6b89d3eccc394e7e902bf9fe787d2375d4f4436d Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Sun, 1 May 2022 16:21:53 +1000 Subject: [PATCH 04/13] Remove commented code --- mne/io/snirf/_snirf.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/mne/io/snirf/_snirf.py b/mne/io/snirf/_snirf.py index 2261f018e3c..e95ff18fbd6 100644 --- a/mne/io/snirf/_snirf.py +++ b/mne/io/snirf/_snirf.py @@ -415,12 +415,6 @@ def natural_keys(text): # 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) From ffa38dbe454a21cae3fb4ef39752da69433f08fb Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Sun, 1 May 2022 16:36:10 +1000 Subject: [PATCH 05/13] More tests --- mne/io/snirf/tests/test_snirf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mne/io/snirf/tests/test_snirf.py b/mne/io/snirf/tests/test_snirf.py index 9ab06c937a3..ff77dae8793 100644 --- a/mne/io/snirf/tests/test_snirf.py +++ b/mne/io/snirf/tests/test_snirf.py @@ -91,6 +91,8 @@ def test_snirf_gowerlabs(): # Test data import 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 From 7edec29d3b2b28bcfdc11129d7421e0dc8f1be54 Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Sun, 1 May 2022 16:43:01 +1000 Subject: [PATCH 06/13] Report api change --- doc/changes/latest.inc | 4 +++- mne/io/snirf/tests/test_snirf.py | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index 7940dc9652f..a09794dd4e3 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -51,7 +51,7 @@ Enhancements - Add ``'voronoi'`` as an option for the ``image_interp`` argument in :func:`mne.viz.plot_topomap` to plot a topomap without interpolation using a Voronoi parcelation (:gh:`10571` by `Alex Rockhill`_) -- Add support for reading data from Gowerlabs devices to :func:`mne.io.read_raw_snirf` (:gh:`10544` by `Robert Luke`_ and `Samuel Powell`_) +- Add support for reading data from Gowerlabs devices to :func:`mne.io.read_raw_snirf` (:gh:`10555` by `Robert Luke`_ and `Samuel Powell`_) Bugs ~~~~ @@ -84,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`_) \ No newline at end of file diff --git a/mne/io/snirf/tests/test_snirf.py b/mne/io/snirf/tests/test_snirf.py index ff77dae8793..472f0d85b46 100644 --- a/mne/io/snirf/tests/test_snirf.py +++ b/mne/io/snirf/tests/test_snirf.py @@ -88,7 +88,6 @@ def test_snirf_gowerlabs(): """Test reading SNIRF files.""" raw = read_raw_snirf(lumo110, preload=True) - # Test data import assert raw._data.shape == (216, 274) assert raw.info['dig'][0]['coord_frame'] == FIFF.FIFFV_COORD_HEAD assert len(raw.ch_names) == 216 From 9fb918cc45b5108fd2b280b5e1889d8871f02ca2 Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Sun, 1 May 2022 19:00:14 +1000 Subject: [PATCH 07/13] Update docs --- mne/io/snirf/_snirf.py | 25 ++++++++++++++++--------- tutorials/io/30_reading_fnirs_data.py | 5 +++-- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/mne/io/snirf/_snirf.py b/mne/io/snirf/_snirf.py index e95ff18fbd6..b5763d69fbd 100644 --- a/mne/io/snirf/_snirf.py +++ b/mne/io/snirf/_snirf.py @@ -272,15 +272,11 @@ def natural_keys(text): # Update info info.update(subject_info=subject_info) - LengthUnit = _get_metadata_str(dat, "LengthUnit") - 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 @@ -322,7 +318,7 @@ def natural_keys(text): if 'landmarkPos3D' in dat.get('nirs/probe/'): diglocs = np.array(dat.get('/nirs/probe/landmarkPos3D')) - diglocs /= scal + diglocs /= length_scaling digname = np.array(dat.get('/nirs/probe/landmarkLabels')) nasion, lpa, rpa, hpi = None, None, None, None extra_ps = dict() @@ -449,6 +445,17 @@ def _get_timeunit_scaling(time_unit): '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')) diff --git a/tutorials/io/30_reading_fnirs_data.py b/tutorials/io/30_reading_fnirs_data.py index 3215c26ea63..c032d6c3f33 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. *********************** From d28ecb702f61f556347aac4bf83c406f6996ccb2 Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Sun, 1 May 2022 19:09:57 +1000 Subject: [PATCH 08/13] Flake --- tutorials/io/30_reading_fnirs_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/io/30_reading_fnirs_data.py b/tutorials/io/30_reading_fnirs_data.py index c032d6c3f33..15f63e4a678 100644 --- a/tutorials/io/30_reading_fnirs_data.py +++ b/tutorials/io/30_reading_fnirs_data.py @@ -40,7 +40,7 @@ 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 Gowerlabs, NIRx, Kernel, and Cortivision +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 From b1486d61b066ba72be652fe75d95d8f36c582037 Mon Sep 17 00:00:00 2001 From: Rob Luke Date: Sun, 1 May 2022 20:52:37 +1000 Subject: [PATCH 09/13] Update config.py --- mne/datasets/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/datasets/config.py b/mne/datasets/config.py index b9132f4fb11..e3b0eef0348 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"]}' From 5c0b0577fc5b331642e0c2537e56ff0b72d49931 Mon Sep 17 00:00:00 2001 From: Rob Luke Date: Sun, 1 May 2022 21:00:46 +1000 Subject: [PATCH 10/13] Update config.py --- mne/datasets/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/datasets/config.py b/mne/datasets/config.py index e3b0eef0348..d4a35fbd594 100644 --- a/mne/datasets/config.py +++ b/mne/datasets/config.py @@ -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', From 07c4c9b0d4cb6e1e4609e127fac3fad42670e98c Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Thu, 5 May 2022 09:07:33 +1000 Subject: [PATCH 11/13] Update test indicies --- doc/changes/latest.inc | 4 +- mne/io/nirx/tests/test_nirx.py | 130 ++++++++++++++++--------------- mne/io/snirf/tests/test_snirf.py | 1 - 3 files changed, 70 insertions(+), 65 deletions(-) diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index a09794dd4e3..a660481663f 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`_) @@ -51,8 +53,6 @@ Enhancements - Add ``'voronoi'`` as an option for the ``image_interp`` argument in :func:`mne.viz.plot_topomap` to plot a topomap without interpolation using a Voronoi parcelation (:gh:`10571` by `Alex Rockhill`_) -- Add support for reading data from Gowerlabs devices to :func:`mne.io.read_raw_snirf` (:gh:`10555` by `Robert Luke`_ and `Samuel Powell`_) - Bugs ~~~~ - Make ``color`` parameter check in in :func:`mne.viz.plot_evoked_topo` consistent (:gh:`10217` by :newcontrib:`T. Wang` and `Stefan Appelhoff`_) 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/tests/test_snirf.py b/mne/io/snirf/tests/test_snirf.py index 472f0d85b46..9709976f198 100644 --- a/mne/io/snirf/tests/test_snirf.py +++ b/mne/io/snirf/tests/test_snirf.py @@ -106,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_D8 760", "S5_D8 850"] # Test frequency encoding assert raw.info['chs'][0]['loc'][9] == 760 From a8d6b915515a6370828d18a5033c41ec720b6040 Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Thu, 5 May 2022 09:25:24 +1000 Subject: [PATCH 12/13] More tests --- mne/channels/tests/test_interpolation.py | 2 +- .../nirs/tests/test_beer_lambert_law.py | 17 +++++++++++++---- ...t_temporal_derivative_distribution_repair.py | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) 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/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. From 59a5c2781bda92ad10701013728f6db5f1541186 Mon Sep 17 00:00:00 2001 From: Rob Luke Date: Thu, 5 May 2022 12:58:02 +1000 Subject: [PATCH 13/13] Update latest.inc --- doc/changes/latest.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index a660481663f..4578d4123e2 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -85,4 +85,4 @@ 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`_) \ No newline at end of file +- 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`_)