From e0f8cc64fbdd67e7169af324e6eb4af23099f657 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Wed, 17 Mar 2021 08:21:25 +0000 Subject: [PATCH 01/44] Add raw montage files sent by Artinis --- .../data/montages/artinis-brite23.txt | 59 +++++++++++++++++++ .../data/montages/artinis-octamon.txt | 35 +++++++++++ 2 files changed, 94 insertions(+) create mode 100644 mne/channels/data/montages/artinis-brite23.txt create mode 100644 mne/channels/data/montages/artinis-octamon.txt diff --git a/mne/channels/data/montages/artinis-brite23.txt b/mne/channels/data/montages/artinis-brite23.txt new file mode 100644 index 00000000000..68bf3d67ed5 --- /dev/null +++ b/mne/channels/data/montages/artinis-brite23.txt @@ -0,0 +1,59 @@ + +MNI Coordinates + +Brain Atlas Coordinate System Projection Method +MNI ICBM 152 nonlinear asymmetric 2009a MNI Coordinate System Scalp Projection + +Fiducial x-Coordinate y-Coordinate z-Coordinate +Nz -4.62 82.33 -45.74 +Iz -4.90 -115.92 -31.92 +RPA 79.66 -18.72 -45.89 +LPA -81.41 -17.18 -45.56 +Cz 2.81 -12.59 96.67 + +Optode x-Coordinate y-Coordinate z-Coordinate +Rx1 65.18 27.28 35.31 +Rx2 48.62 59.71 22.68 +Rx3 18.95 72.41 38.32 +Rx4 -3.97 79.74 30.28 +Rx5 -25.96 72.19 35.16 +Rx6 -52.51 60.53 14.54 +Rx7 -66.37 32.04 31.08 + +Optode x-Coordinate y-Coordinate z-Coordinate +Tx1 76.10 -0.29 31.24 +Tx2 65.61 -0.26 56.15 +Tx3 64.93 42.43 8.29 +Tx4 43.32 46.36 50.77 +Tx5 21.58 82.45 1.06 +Tx6 -2.91 59.57 61.59 +Tx7 -29.62 79.35 2.38 +Tx8 -48.13 44.76 49.15 +Tx9 -67.68 43.26 -3.18 +Tx10 -65.37 4.89 56.36 +Tx11 -77.24 5.88 27.58 + +Channel x-Coordinate y-Coordinate z-Coordinate +Rx1-Tx1 70.71 14.91 34.96 +Rx1-Tx2 65.96 11.27 48.41 +Rx1-Tx3 65.53 36.16 21.42 +Rx1-Tx4 55.49 36.06 44.50 +Rx2-Tx3 57.64 52.09 13.64 +Rx2-Tx4 46.47 52.98 38.87 +Rx2-Tx5 36.77 72.78 13.84 +Rx3-Tx4 34.91 59.03 45.54 +Rx3-Tx5 20.83 78.65 22.34 +Rx3-Tx6 7.44 68.09 50.48 +Rx4-Tx5 8.37 83.62 17.02 +Rx4-Tx6 -3.95 71.80 45.98 +Rx4-Tx7 -16.94 82.98 14.76 +Rx5-Tx6 -18.12 67.26 48.34 +Rx5-Tx7 -29.46 77.35 17.29 +Rx5-Tx8 -38.28 58.97 44.13 +Rx6-Tx7 -43.67 70.05 8.04 +Rx6-Tx8 -51.14 53.07 33.96 +Rx6-Tx9 -61.17 52.21 6.64 +Rx7-Tx8 -57.77 39.15 40.61 +Rx7-Tx9 -68.49 37.56 14.92 +Rx7-Tx10 -67.44 19.36 42.90 +Rx7-Tx11 -73.47 16.23 31.14 \ No newline at end of file diff --git a/mne/channels/data/montages/artinis-octamon.txt b/mne/channels/data/montages/artinis-octamon.txt new file mode 100644 index 00000000000..8f86f7098ff --- /dev/null +++ b/mne/channels/data/montages/artinis-octamon.txt @@ -0,0 +1,35 @@ +MNI Coordinates + +Brain Atlas Coordinate System Projection Method +MNI ICBM 152 nonlinear asymmetric 2009a MNI Coordinate System Scalp Projection + +Fiducial x-Coordinate y-Coordinate z-Coordinate +Nz 0.96 83.56 -48.63 +Iz 0.33 -115.25 -34.65 +RPA 80.25 -19.67 -43.88 +LPA -82.58 -20.09 -43.10 +Cz 0.65 -12.86 96.78 + +Optode x-Coordinate y-Coordinate z-Coordinate +Rx1 47.77 65.28 7.28 +Rx2 -46.45 67.76 8.81 + +Optode x-Coordinate y-Coordinate z-Coordinate +Tx1 63.88 34.84 28.34 +Tx2 64.96 45.02 -10.31 +Tx3 22.07 74.86 31.03 +Tx4 17.84 84.96 -10.84 +Tx5 -10.81 77.96 32.10 +Tx6 -15.96 85.24 -7.41 +Tx7 -61.78 40.78 29.92 +Tx8 -65.28 48.14 -10.73 + +Channel x-Coordinate y-Coordinate z-Coordinate +Rx1-Tx1 57.00 50.56 19.33 +Rx1-Tx2 57.12 56.10 1.26 +Rx1-Tx3 38.41 70.37 19.56 +Rx1-Tx4 32.83 76.89 -0.91 +Rx2-Tx5 -31.45 75.44 21.04 +Rx2-Tx6 -33.04 77.66 0.59 +Rx2-Tx7 -56.11 54.37 19.49 +Rx2-Tx8 -55.73 60.39 -1.00 \ No newline at end of file From 2159fb311f1397828a89813879b02969b4c53512 Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Wed, 17 Mar 2021 21:12:38 +1100 Subject: [PATCH 02/44] Initial pseudo code --- mne/channels/_standard_montage_utils.py | 2 ++ .../data/montages/artinis-octomon.elc | 20 +++++++++++++++++ mne/channels/tests/test_standard_montage.py | 22 +++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 mne/channels/data/montages/artinis-octomon.elc diff --git a/mne/channels/_standard_montage_utils.py b/mne/channels/_standard_montage_utils.py index bc4730d261d..7874264dfa6 100644 --- a/mne/channels/_standard_montage_utils.py +++ b/mne/channels/_standard_montage_utils.py @@ -142,6 +142,8 @@ def _mgh_or_standard(basename, head_size): basename='standard_prefixed.elc'), 'standard_primed': partial(_mgh_or_standard, basename='standard_primed.elc'), + 'artinis-octomon': partial(_mgh_or_standard, + basename='artinis-octomon.elc'), } diff --git a/mne/channels/data/montages/artinis-octomon.elc b/mne/channels/data/montages/artinis-octomon.elc new file mode 100644 index 00000000000..0d502e57207 --- /dev/null +++ b/mne/channels/data/montages/artinis-octomon.elc @@ -0,0 +1,20 @@ +# ASA electrode file +ReferenceLabel avg +UnitPosition mm +NumberPositions= 77 +Positions +-82.58 -20.09 -43.10 +80.25 -19.67 -43.88 +0.96 83.56 -48.63 +47.77 65.28 7.28 +-46.45 67.76 8.81 +63.88 34.84 28.34 +64.96 45.02 -10.31 +Labels +LPA +RPA +Nz +D1 +D2 +S1 +S2 \ No newline at end of file diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index eed6a2670ca..6ee5c668f4d 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -73,3 +73,25 @@ def test_standard_superset(): for key, value in m_1020._get_ch_pos().items(): if key not in ('O10', 'O9'): assert_allclose(c_1005[key], value, atol=1e-4, err_msg=key) + + +def test_artinis(): + a = make_standard_montage("artinis-octomon") + assert a.ch_names == ['D1', 'D2', 'S1', 'S2'] + + import mne + import os + fnirs_data_folder = mne.datasets.fnirs_motor.data_path() + fnirs_cw_amplitude_dir = os.path.join(fnirs_data_folder, 'Participant-1') + d = mne.io.read_raw_nirx(fnirs_cw_amplitude_dir, verbose=True) + d.load_data() + d.plot_sensors() + + d.pick(picks=["S1_D1 760", "S1_D1 850", "S1_D2 760", "S1_D2 850", + "S2_D1 760", "S2_D1 850", "S2_D2 760", "S2_D2 850"]) + d.info["chs"][0] + + + import + d.set_montage(a) + From 44cbe022df12854733a8cff3265055f115d86f07 Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Wed, 17 Mar 2021 21:14:58 +1100 Subject: [PATCH 03/44] Tweaks --- mne/channels/montage.py | 16 +++++++++++++++- mne/channels/tests/test_standard_montage.py | 5 +++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index 996ac2bc997..fd7e1d1ae01 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -51,7 +51,8 @@ 'easycap-M1', 'easycap-M10', 'mgh60', 'mgh70', 'standard_1005', 'standard_1020', 'standard_alphabetic', - 'standard_postfixed', 'standard_prefixed', 'standard_primed' + 'standard_postfixed', 'standard_prefixed', 'standard_primed', + 'artinis-octomon' ] @@ -666,6 +667,15 @@ def _get_montage_in_head(montage): return transform_to_head(montage.copy()) +def _set_montage_fnirs(info, montage): + + # for ch in info.channels: + + return 1 + + + + @fill_doc def _set_montage(info, montage, match_case=True, match_alias=False, on_missing='raise'): @@ -841,6 +851,10 @@ def _backcompat_value(pos, ref_pos): for ch in info['chs']: ch['loc'] = np.full(12, np.nan) + # if ch_type contains fnirs: + # info = _set_montage_fnirs(data) + # info + def _read_isotrak_elp_points(fname): """Read Polhemus Isotrak digitizer data from a ``.elp`` file. diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index 6ee5c668f4d..df7d0367a4d 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -81,6 +81,7 @@ def test_artinis(): import mne import os + from mne.channels.montage import _set_montage_fnirs fnirs_data_folder = mne.datasets.fnirs_motor.data_path() fnirs_cw_amplitude_dir = os.path.join(fnirs_data_folder, 'Participant-1') d = mne.io.read_raw_nirx(fnirs_cw_amplitude_dir, verbose=True) @@ -92,6 +93,6 @@ def test_artinis(): d.info["chs"][0] - import - d.set_montage(a) + + _set_montage_fnirs(d, a) From 5dae7fa4e4557c941e1a230ee29492774d26e5ed Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Wed, 17 Mar 2021 10:53:06 +0000 Subject: [PATCH 04/44] Draft _set_montage_fnirs --- mne/channels/montage.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index fd7e1d1ae01..613df1b4a9e 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -669,9 +669,19 @@ def _get_montage_in_head(montage): def _set_montage_fnirs(info, montage): - # for ch in info.channels: + for ch_idx, ch in enumerate(info['ch_names']): + source, detector = ch.split(' ')[0].split('_') + source_pos = montage.dig[montage.ch_names.index(source) + 3]['r'] + detector_pos = montage.dig[montage.ch_names.index(detector) + 3]['r'] + + info['chs'][ch_idx]['loc'][3:6] = source_pos + info['chs'][ch_idx]['loc'][6:9] = detector_pos + + midpoint = (source_pos + detector_pos) / 2 + info['chs'][ch_idx]['loc'][:3] = midpoint + + return info - return 1 From e55859a172f45cb7c7b86ffc348ecd42e820f32d Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Wed, 17 Mar 2021 22:47:56 +1100 Subject: [PATCH 05/44] Tweaks --- mne/channels/tests/test_standard_montage.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index df7d0367a4d..db353f0798f 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -89,10 +89,10 @@ def test_artinis(): d.plot_sensors() d.pick(picks=["S1_D1 760", "S1_D1 850", "S1_D2 760", "S1_D2 850", - "S2_D1 760", "S2_D1 850", "S2_D2 760", "S2_D2 850"]) - d.info["chs"][0] - + "S2_D1 760", "S2_D1 850"]) + d.plot_sensors() - _set_montage_fnirs(d, a) + d.info = _set_montage_fnirs(d.info, a) + d.plot_sensors() From cebdf107697546f0fb531ceb8a6644917a69b17d Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Wed, 17 Mar 2021 15:17:00 +0000 Subject: [PATCH 06/44] Example with optode montage as elc file --- .../data/montages/artinis-octomon.elc | 26 ++++++++++++++----- mne/channels/montage.py | 9 ++----- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/mne/channels/data/montages/artinis-octomon.elc b/mne/channels/data/montages/artinis-octomon.elc index 0d502e57207..7bd8443c305 100644 --- a/mne/channels/data/montages/artinis-octomon.elc +++ b/mne/channels/data/montages/artinis-octomon.elc @@ -1,20 +1,32 @@ -# ASA electrode file +# ASA optode file ReferenceLabel avg UnitPosition mm -NumberPositions= 77 +NumberPositions= 15 Positions --82.58 -20.09 -43.10 -80.25 -19.67 -43.88 0.96 83.56 -48.63 +80.25 -19.67 -43.88 +-82.58 -20.09 -43.10 47.77 65.28 7.28 -46.45 67.76 8.81 63.88 34.84 28.34 64.96 45.02 -10.31 +22.07 74.86 31.03 +17.84 84.96 -10.84 +-10.81 77.96 32.10 +-15.96 85.24 -7.41 +-61.78 40.78 29.92 +-65.28 48.14 -10.73 Labels -LPA -RPA Nz +RPA +LPA D1 D2 S1 -S2 \ No newline at end of file +S2 +S3 +S4 +S5 +S6 +S7 +S8 diff --git a/mne/channels/montage.py b/mne/channels/montage.py index 613df1b4a9e..443a53a1cbb 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -673,19 +673,15 @@ def _set_montage_fnirs(info, montage): source, detector = ch.split(' ')[0].split('_') source_pos = montage.dig[montage.ch_names.index(source) + 3]['r'] detector_pos = montage.dig[montage.ch_names.index(detector) + 3]['r'] - + info['chs'][ch_idx]['loc'][3:6] = source_pos info['chs'][ch_idx]['loc'][6:9] = detector_pos - midpoint = (source_pos + detector_pos) / 2 info['chs'][ch_idx]['loc'][:3] = midpoint return info - - - @fill_doc def _set_montage(info, montage, match_case=True, match_alias=False, on_missing='raise'): @@ -862,8 +858,7 @@ def _backcompat_value(pos, ref_pos): ch['loc'] = np.full(12, np.nan) # if ch_type contains fnirs: - # info = _set_montage_fnirs(data) - # info + # info = _set_montage_fnirs(info, montage) def _read_isotrak_elp_points(fname): From 90648835e71cc3d2a86c3c7205b802d2469efa26 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Wed, 17 Mar 2021 22:07:49 +0000 Subject: [PATCH 07/44] Clean up and add Brite 23 --- mne/channels/_standard_montage_utils.py | 6 ++- .../data/montages/artinis-brite23.elc | 48 +++++++++++++++++++ .../data/montages/artinis-brite23.txt | 3 +- ...rtinis-octomon.elc => artinis-octamon.elc} | 2 +- .../data/montages/artinis-octamon.txt | 2 +- mne/channels/montage.py | 6 ++- mne/channels/tests/test_standard_montage.py | 28 +++++------ 7 files changed, 73 insertions(+), 22 deletions(-) create mode 100644 mne/channels/data/montages/artinis-brite23.elc rename mne/channels/data/montages/{artinis-octomon.elc => artinis-octamon.elc} (94%) diff --git a/mne/channels/_standard_montage_utils.py b/mne/channels/_standard_montage_utils.py index 7874264dfa6..f5723c1805e 100644 --- a/mne/channels/_standard_montage_utils.py +++ b/mne/channels/_standard_montage_utils.py @@ -142,8 +142,10 @@ def _mgh_or_standard(basename, head_size): basename='standard_prefixed.elc'), 'standard_primed': partial(_mgh_or_standard, basename='standard_primed.elc'), - 'artinis-octomon': partial(_mgh_or_standard, - basename='artinis-octomon.elc'), + 'artinis-octamon': partial(_mgh_or_standard, + basename='artinis-octamon.elc'), + 'artinis-brite23': partial(_mgh_or_standard, + basename='artinis-brite23.elc'), } diff --git a/mne/channels/data/montages/artinis-brite23.elc b/mne/channels/data/montages/artinis-brite23.elc new file mode 100644 index 00000000000..dea8b7aa80a --- /dev/null +++ b/mne/channels/data/montages/artinis-brite23.elc @@ -0,0 +1,48 @@ +# ASA optode file +ReferenceLabel avg +UnitPosition mm +NumberPositions= 21 +Positions +-4.62 82.33 -45.74 +79.66 -18.72 -45.89 +-81.41 -17.18 -45.56 +65.18 27.28 35.31 +48.62 59.71 22.68 +18.95 72.41 38.32 +-3.97 79.74 30.28 +-25.96 72.19 35.16 +-52.51 60.53 14.54 +-66.37 32.04 31.08 +76.10 -0.29 31.24 +65.61 -0.26 56.15 +64.93 42.43 8.29 +43.32 46.36 50.77 +21.58 82.45 1.06 +-2.91 59.57 61.59 +-29.62 79.35 2.38 +-48.13 44.76 49.15 +-67.68 43.26 -3.18 +-65.37 4.89 56.36 +-77.24 5.88 27.58 +Labels +Nz +RPA +LPA +Rx1 +Rx2 +Rx3 +Rx4 +Rx5 +Rx6 +Rx7 +Tx1 +Tx2 +Tx3 +Tx4 +Tx5 +Tx6 +Tx7 +Tx8 +Tx9 +Tx10 +Tx11 diff --git a/mne/channels/data/montages/artinis-brite23.txt b/mne/channels/data/montages/artinis-brite23.txt index 68bf3d67ed5..a47b4c3cad1 100644 --- a/mne/channels/data/montages/artinis-brite23.txt +++ b/mne/channels/data/montages/artinis-brite23.txt @@ -1,4 +1,3 @@ - MNI Coordinates Brain Atlas Coordinate System Projection Method @@ -56,4 +55,4 @@ Rx6-Tx9 -61.17 52.21 6.64 Rx7-Tx8 -57.77 39.15 40.61 Rx7-Tx9 -68.49 37.56 14.92 Rx7-Tx10 -67.44 19.36 42.90 -Rx7-Tx11 -73.47 16.23 31.14 \ No newline at end of file +Rx7-Tx11 -73.47 16.23 31.14 diff --git a/mne/channels/data/montages/artinis-octomon.elc b/mne/channels/data/montages/artinis-octamon.elc similarity index 94% rename from mne/channels/data/montages/artinis-octomon.elc rename to mne/channels/data/montages/artinis-octamon.elc index 7bd8443c305..748a19d04e1 100644 --- a/mne/channels/data/montages/artinis-octomon.elc +++ b/mne/channels/data/montages/artinis-octamon.elc @@ -1,7 +1,7 @@ # ASA optode file ReferenceLabel avg UnitPosition mm -NumberPositions= 15 +NumberPositions= 13 Positions 0.96 83.56 -48.63 80.25 -19.67 -43.88 diff --git a/mne/channels/data/montages/artinis-octamon.txt b/mne/channels/data/montages/artinis-octamon.txt index 8f86f7098ff..e594631be75 100644 --- a/mne/channels/data/montages/artinis-octamon.txt +++ b/mne/channels/data/montages/artinis-octamon.txt @@ -32,4 +32,4 @@ Rx1-Tx4 32.83 76.89 -0.91 Rx2-Tx5 -31.45 75.44 21.04 Rx2-Tx6 -33.04 77.66 0.59 Rx2-Tx7 -56.11 54.37 19.49 -Rx2-Tx8 -55.73 60.39 -1.00 \ No newline at end of file +Rx2-Tx8 -55.73 60.39 -1.00 diff --git a/mne/channels/montage.py b/mne/channels/montage.py index 443a53a1cbb..e8bc35cef36 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -52,7 +52,7 @@ 'mgh60', 'mgh70', 'standard_1005', 'standard_1020', 'standard_alphabetic', 'standard_postfixed', 'standard_prefixed', 'standard_primed', - 'artinis-octomon' + 'artinis-octamon', 'artinis-brite23' ] @@ -1316,6 +1316,10 @@ def make_standard_montage(kind, head_size=HEAD_SIZE_DEFAULT): MGH (60+3 locations) mgh70 The (newer) 70-channel BrainVision cap used at MGH (70+3 locations) + + artinis-octamon Artinis OctaMon fNIRS (8 sources, 2 detectors) + + artinis-brite23 Artinis Brite 23 fNIRS (11 sources, 7 detectors) =================== ===================================================== .. versionadded:: 0.19.0 diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index db353f0798f..d6e91128834 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -76,23 +76,21 @@ def test_standard_superset(): def test_artinis(): - a = make_standard_montage("artinis-octomon") - assert a.ch_names == ['D1', 'D2', 'S1', 'S2'] - import mne import os from mne.channels.montage import _set_montage_fnirs + fnirs_data_folder = mne.datasets.fnirs_motor.data_path() fnirs_cw_amplitude_dir = os.path.join(fnirs_data_folder, 'Participant-1') - d = mne.io.read_raw_nirx(fnirs_cw_amplitude_dir, verbose=True) - d.load_data() - d.plot_sensors() - - d.pick(picks=["S1_D1 760", "S1_D1 850", "S1_D2 760", "S1_D2 850", - "S2_D1 760", "S2_D1 850"]) - - d.plot_sensors() - - d.info = _set_montage_fnirs(d.info, a) - d.plot_sensors() - + raw = mne.io.read_raw_nirx(fnirs_cw_amplitude_dir, preload=True, + verbose=True) + raw.plot_sensors() + + raw.pick(picks=["S1_D1 760", "S1_D1 850", + "S1_D2 760", "S1_D2 850", + "S2_D1 760", "S2_D1 850"]) + raw.plot_sensors() + + montage = make_standard_montage("artinis-octamon") + raw.info = _set_montage_fnirs(raw.info, montage) + raw.plot_sensors() From 9bbf7a8cd185efa2c866616704e0911750eda8ee Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Thu, 18 Mar 2021 17:48:25 +1100 Subject: [PATCH 08/44] Tweak skeleton code and push example --- mne/channels/montage.py | 26 +++++++--- mne/channels/tests/test_standard_montage.py | 46 +++++++++++------- tutorials/io/plot_30_reading_fnirs_data.py | 53 +++++++++++++++++++++ 3 files changed, 102 insertions(+), 23 deletions(-) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index e8bc35cef36..28f70e6122d 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -32,7 +32,7 @@ _get_data_as_dict_from_dig) from ..io.meas_info import create_info from ..io.open import fiff_open -from ..io.pick import pick_types +from ..io.pick import pick_types, _picks_to_idx from ..io.constants import FIFF, CHANNEL_LOC_ALIASES from ..utils import (warn, copy_function_doc_to_method_doc, _pl, verbose, _check_option, _validate_type, _check_fname, _on_missing, @@ -668,11 +668,22 @@ def _get_montage_in_head(montage): def _set_montage_fnirs(info, montage): + """ + Sets the montage for fNIRS data. This needs to be different to electrodes + as each channel has three coordinates that need to be set. For each channel + there is a source optode location, a detector optode location, + and a channel midpoint that must be stored. + """ + num_ficiduals = len(montage.dig) - len(montage.ch_names) + picks = _picks_to_idx(info, 'fnirs', exclude=[], allow_empty=True) - for ch_idx, ch in enumerate(info['ch_names']): + for ch_idx in picks: + ch = info['ch_names'][ch_idx] source, detector = ch.split(' ')[0].split('_') - source_pos = montage.dig[montage.ch_names.index(source) + 3]['r'] - detector_pos = montage.dig[montage.ch_names.index(detector) + 3]['r'] + source_pos = montage.dig[montage.ch_names.index(source) + + num_ficiduals]['r'] + detector_pos = montage.dig[montage.ch_names.index(detector) + + num_ficiduals]['r'] info['chs'][ch_idx]['loc'][3:6] = source_pos info['chs'][ch_idx]['loc'][6:9] = detector_pos @@ -852,14 +863,15 @@ def _backcompat_value(pos, ref_pos): if mnt_head.dev_head_t is not None: info['dev_head_t'] = Transform('meg', 'head', mnt_head.dev_head_t) + fnirs_picks = _picks_to_idx(info, 'fnirs', allow_empty=True) + if len(fnirs_picks) > 0: + info = _set_montage_fnirs(info, montage) + else: # None case info['dig'] = None for ch in info['chs']: ch['loc'] = np.full(12, np.nan) - # if ch_type contains fnirs: - # info = _set_montage_fnirs(info, montage) - def _read_isotrak_elp_points(fname): """Read Polhemus Isotrak digitizer data from a ``.elp`` file. diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index d6e91128834..a3fc2906277 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -75,22 +75,36 @@ def test_standard_superset(): assert_allclose(c_1005[key], value, atol=1e-4, err_msg=key) -def test_artinis(): - import mne - import os - from mne.channels.montage import _set_montage_fnirs - - fnirs_data_folder = mne.datasets.fnirs_motor.data_path() - fnirs_cw_amplitude_dir = os.path.join(fnirs_data_folder, 'Participant-1') - raw = mne.io.read_raw_nirx(fnirs_cw_amplitude_dir, preload=True, - verbose=True) - raw.plot_sensors() - +def _simulate_artinis_octomon(): + """ + Simulate artinis octomon channel data from nirx data. + This is to test data that is imported with missing or incorrect montage + info. This data can then me used to test the set_montage function. + """ + import os.path as op + from mne.datasets import fnirs_motor + from mne.io import read_raw_nirx + fnirs_data_folder = fnirs_motor.data_path() + fnirs_cw_amplitude_dir = op.join(fnirs_data_folder, 'Participant-1') + raw = read_raw_nirx(fnirs_cw_amplitude_dir, preload=True) + mapping = {'S1_D2 760': 'S3_D1 760', 'S1_D2 850': 'S3_D1 850', + 'S1_D3 760': 'S4_D1 760', 'S1_D3 850': 'S4_D1 850', + 'S1_D9 760': 'S5_D2 760', 'S1_D9 850': 'S5_D2 850', + 'S2_D3 760': 'S6_D2 760', 'S2_D3 850': 'S6_D2 850', + 'S2_D4 760': 'S7_D2 760', 'S2_D4 850': 'S7_D2 850', + 'S2_D10 760': 'S8_D2 760', 'S2_D10 850': 'S8_D2 850'} + raw.rename_channels(mapping) raw.pick(picks=["S1_D1 760", "S1_D1 850", - "S1_D2 760", "S1_D2 850", - "S2_D1 760", "S2_D1 850"]) - raw.plot_sensors() + "S2_D1 760", "S2_D1 850", + "S3_D1 760", "S3_D1 850", + "S4_D1 760", "S4_D1 850", + "S5_D2 760", "S5_D2 850", + "S6_D2 760", "S6_D2 850", + "S7_D2 760", "S7_D2 850", + "S8_D2 760", "S8_D2 850"]) + return raw +def test_artinis(): + raw = _simulate_artinis_octomon() montage = make_standard_montage("artinis-octamon") - raw.info = _set_montage_fnirs(raw.info, montage) - raw.plot_sensors() + raw.set_montage(montage) diff --git a/tutorials/io/plot_30_reading_fnirs_data.py b/tutorials/io/plot_30_reading_fnirs_data.py index 1bb8d851ea0..03ec147de9c 100644 --- a/tutorials/io/plot_30_reading_fnirs_data.py +++ b/tutorials/io/plot_30_reading_fnirs_data.py @@ -74,3 +74,56 @@ recommended. """ # noqa:E501 + + + + +############################################################################### +# Fake some data +# -------------- +# +# Here we just create some fake data with the correct names for an +# artinis octomon system + +import mne +from mne.channels import make_standard_montage +from mne.channels.tests.test_standard_montage import _simulate_artinis_octomon + +raw_intensity = _simulate_artinis_octomon() + + +############################################################################### +# Next we load the montage +# ------------------------------------------- +# +# And apply montage to data + +montage = make_standard_montage("artinis-octamon") +raw_intensity.set_montage(montage) + + +############################################################################### +# View location of sensors over brain surface +# ------------------------------------------- +# +# Here we validate + +subjects_dir = mne.datasets.sample.data_path() + '/subjects' + +fig = mne.viz.create_3d_figure(size=(800, 600), bgcolor='white') +fig = mne.viz.plot_alignment(raw_intensity.info, show_axes=True, + subject='fsaverage', coord_frame='mri', + trans='fsaverage', surfaces=['brain'], + fnirs=['channels', 'pairs', + 'sources', 'detectors'], + subjects_dir=subjects_dir, fig=fig) +mne.viz.set_3d_view(figure=fig, azimuth=70, elevation=100, distance=0.4, + focalpoint=(0., -0.01, 0.02)) + + + +############################################################################### +# +# These locations look wrong to me. I think we might have a coorinate system +# clash somewhere. + From d65d8f8f08c9890bc3a5cfe8b862105a5d583ebc Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Thu, 18 Mar 2021 10:31:02 +0000 Subject: [PATCH 09/44] Add TODOs --- mne/channels/montage.py | 12 +++++++++++- mne/channels/tests/test_standard_montage.py | 8 ++++---- tutorials/io/plot_30_reading_fnirs_data.py | 6 +++--- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index 28f70e6122d..c8d286ad5e8 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -674,9 +674,14 @@ def _set_montage_fnirs(info, montage): there is a source optode location, a detector optode location, and a channel midpoint that must be stored. """ + # TODO [x] info['chs'][_]['loc'] + # TODO [ ] info['dig'] + # TODO [ ] info['dev_head_t'] + mnt_head = _get_montage_in_head(montage) + + num_ficiduals = len(montage.dig) - len(montage.ch_names) picks = _picks_to_idx(info, 'fnirs', exclude=[], allow_empty=True) - for ch_idx in picks: ch = info['ch_names'][ch_idx] source, detector = ch.split(' ')[0].split('_') @@ -830,6 +835,7 @@ def _backcompat_value(pos, ref_pos): for name, use in zip(info_names, info_names_use): _loc_view = info['chs'][info['ch_names'].index(name)]['loc'] + # XXX info['chs'][_]['loc'] modified in place _loc_view[:6] = _backcompat_value(ch_pos_use[use], eeg_ref_pos) del ch_pos_use @@ -858,9 +864,11 @@ def _backcompat_value(pos, ref_pos): # in the old dig if ref_dig_point in old_dig: digpoints.append(ref_dig_point) + # XXX info['dig'] modified in place info['dig'] = _format_dig_points(digpoints, enforce_order=True) if mnt_head.dev_head_t is not None: + # XXX info['dev_head_t'] modified in place info['dev_head_t'] = Transform('meg', 'head', mnt_head.dev_head_t) fnirs_picks = _picks_to_idx(info, 'fnirs', allow_empty=True) @@ -868,8 +876,10 @@ def _backcompat_value(pos, ref_pos): info = _set_montage_fnirs(info, montage) else: # None case + # XXX info['dig'] modified in place info['dig'] = None for ch in info['chs']: + # XXX info['chs'][_]['loc'] modified in place ch['loc'] = np.full(12, np.nan) diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index a3fc2906277..4a67fed5816 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -75,11 +75,11 @@ def test_standard_superset(): assert_allclose(c_1005[key], value, atol=1e-4, err_msg=key) -def _simulate_artinis_octomon(): +def _simulate_artinis_octamon(): """ - Simulate artinis octomon channel data from nirx data. + Simulate artinis octamon channel data from nirx data. This is to test data that is imported with missing or incorrect montage - info. This data can then me used to test the set_montage function. + info. This data can then be used to test the set_montage function. """ import os.path as op from mne.datasets import fnirs_motor @@ -105,6 +105,6 @@ def _simulate_artinis_octomon(): return raw def test_artinis(): - raw = _simulate_artinis_octomon() + raw = _simulate_artinis_octamon() montage = make_standard_montage("artinis-octamon") raw.set_montage(montage) diff --git a/tutorials/io/plot_30_reading_fnirs_data.py b/tutorials/io/plot_30_reading_fnirs_data.py index 03ec147de9c..87abb0678e5 100644 --- a/tutorials/io/plot_30_reading_fnirs_data.py +++ b/tutorials/io/plot_30_reading_fnirs_data.py @@ -83,13 +83,13 @@ # -------------- # # Here we just create some fake data with the correct names for an -# artinis octomon system +# artinis octamon system import mne from mne.channels import make_standard_montage -from mne.channels.tests.test_standard_montage import _simulate_artinis_octomon +from mne.channels.tests.test_standard_montage import _simulate_artinis_octamon -raw_intensity = _simulate_artinis_octomon() +raw_intensity = _simulate_artinis_octamon() ############################################################################### From 7428812369141795ff26d80dc8747a2a81033952 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Thu, 18 Mar 2021 12:51:28 +0000 Subject: [PATCH 10/44] Update also dig --- .../data/montages/artinis-brite23.elc | 36 +++++++++---------- mne/channels/montage.py | 10 +++--- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/mne/channels/data/montages/artinis-brite23.elc b/mne/channels/data/montages/artinis-brite23.elc index dea8b7aa80a..1e14b0a0a2b 100644 --- a/mne/channels/data/montages/artinis-brite23.elc +++ b/mne/channels/data/montages/artinis-brite23.elc @@ -28,21 +28,21 @@ Labels Nz RPA LPA -Rx1 -Rx2 -Rx3 -Rx4 -Rx5 -Rx6 -Rx7 -Tx1 -Tx2 -Tx3 -Tx4 -Tx5 -Tx6 -Tx7 -Tx8 -Tx9 -Tx10 -Tx11 +D1 +D2 +D3 +D4 +D5 +D6 +D7 +S1 +S2 +S3 +S4 +S5 +S6 +S7 +S8 +S9 +S10 +S11 diff --git a/mne/channels/montage.py b/mne/channels/montage.py index c8d286ad5e8..742eae89ff5 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -674,12 +674,7 @@ def _set_montage_fnirs(info, montage): there is a source optode location, a detector optode location, and a channel midpoint that must be stored. """ - # TODO [x] info['chs'][_]['loc'] - # TODO [ ] info['dig'] - # TODO [ ] info['dev_head_t'] - mnt_head = _get_montage_in_head(montage) - - + # Modify info['chs'][_]['loc'] in place num_ficiduals = len(montage.dig) - len(montage.ch_names) picks = _picks_to_idx(info, 'fnirs', exclude=[], allow_empty=True) for ch_idx in picks: @@ -695,6 +690,9 @@ def _set_montage_fnirs(info, montage): midpoint = (source_pos + detector_pos) / 2 info['chs'][ch_idx]['loc'][:3] = midpoint + # Modify info['dig'] in place + info['dig'] = montage.dig + return info From 599bbfd08fedf95eb07c34e9675d1af17f119682 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Thu, 18 Mar 2021 20:41:24 +0000 Subject: [PATCH 11/44] Fix optodes in the brain --- mne/channels/_standard_montage_utils.py | 8 ++++---- mne/channels/montage.py | 3 ++- tutorials/io/plot_30_reading_fnirs_data.py | 10 +++++----- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/mne/channels/_standard_montage_utils.py b/mne/channels/_standard_montage_utils.py index f5723c1805e..c07ddd829d0 100644 --- a/mne/channels/_standard_montage_utils.py +++ b/mne/channels/_standard_montage_utils.py @@ -71,7 +71,7 @@ def _biosemi(basename, head_size): return _read_theta_phi_in_degrees(fname, head_size, fid_names) -def _mgh_or_standard(basename, head_size): +def _mgh_or_standard(basename, head_size, coord_frame='unknown'): fid_names = ('Nz', 'LPA', 'RPA') fname = op.join(MONTAGE_PATH, basename) @@ -101,7 +101,7 @@ def _mgh_or_standard(basename, head_size): lpa *= scale rpa *= scale - return make_dig_montage(ch_pos=ch_pos, coord_frame='unknown', + return make_dig_montage(ch_pos=ch_pos, coord_frame=coord_frame, nasion=nasion, lpa=lpa, rpa=rpa) @@ -142,9 +142,9 @@ def _mgh_or_standard(basename, head_size): basename='standard_prefixed.elc'), 'standard_primed': partial(_mgh_or_standard, basename='standard_primed.elc'), - 'artinis-octamon': partial(_mgh_or_standard, + 'artinis-octamon': partial(_mgh_or_standard, coord_frame='mri', basename='artinis-octamon.elc'), - 'artinis-brite23': partial(_mgh_or_standard, + 'artinis-brite23': partial(_mgh_or_standard, coord_frame='mri', basename='artinis-brite23.elc'), } diff --git a/mne/channels/montage.py b/mne/channels/montage.py index 742eae89ff5..48ce0feac22 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -729,6 +729,7 @@ def _set_montage(info, montage, match_case=True, match_alias=False, if isinstance(montage, DigMontage): mnt_head = _get_montage_in_head(montage) + del montage def _backcompat_value(pos, ref_pos): if any(np.isnan(pos)): @@ -871,7 +872,7 @@ def _backcompat_value(pos, ref_pos): fnirs_picks = _picks_to_idx(info, 'fnirs', allow_empty=True) if len(fnirs_picks) > 0: - info = _set_montage_fnirs(info, montage) + info = _set_montage_fnirs(info, mnt_head) else: # None case # XXX info['dig'] modified in place diff --git a/tutorials/io/plot_30_reading_fnirs_data.py b/tutorials/io/plot_30_reading_fnirs_data.py index 87abb0678e5..0e599414c01 100644 --- a/tutorials/io/plot_30_reading_fnirs_data.py +++ b/tutorials/io/plot_30_reading_fnirs_data.py @@ -112,18 +112,18 @@ fig = mne.viz.create_3d_figure(size=(800, 600), bgcolor='white') fig = mne.viz.plot_alignment(raw_intensity.info, show_axes=True, + dig=True, mri_fiducials=True, subject='fsaverage', coord_frame='mri', - trans='fsaverage', surfaces=['brain'], + trans='fsaverage', surfaces=['brain', 'head'], fnirs=['channels', 'pairs', 'sources', 'detectors'], subjects_dir=subjects_dir, fig=fig) + mne.viz.set_3d_view(figure=fig, azimuth=70, elevation=100, distance=0.4, focalpoint=(0., -0.01, 0.02)) ############################################################################### -# -# These locations look wrong to me. I think we might have a coorinate system -# clash somewhere. - +# TODO Can compare trans to the internal fsaverage-trans.fif as a test +trans = mne.channels.compute_native_head_t(montage) From b77459c67163ad12c16913165e1780b3864e3f9d Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Thu, 18 Mar 2021 21:07:38 +0000 Subject: [PATCH 12/44] Add fetch_fsaverage --- mne/channels/tests/test_standard_montage.py | 1 + tutorials/io/plot_30_reading_fnirs_data.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index 4a67fed5816..cc84d0c91df 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -104,6 +104,7 @@ def _simulate_artinis_octamon(): "S8_D2 760", "S8_D2 850"]) return raw + def test_artinis(): raw = _simulate_artinis_octamon() montage = make_standard_montage("artinis-octamon") diff --git a/tutorials/io/plot_30_reading_fnirs_data.py b/tutorials/io/plot_30_reading_fnirs_data.py index 0e599414c01..681db0dfd29 100644 --- a/tutorials/io/plot_30_reading_fnirs_data.py +++ b/tutorials/io/plot_30_reading_fnirs_data.py @@ -109,6 +109,7 @@ # Here we validate subjects_dir = mne.datasets.sample.data_path() + '/subjects' +mne.datasets.fetch_fsaverage(subjects_dir=subjects_dir, verbose=True) fig = mne.viz.create_3d_figure(size=(800, 600), bgcolor='white') fig = mne.viz.plot_alignment(raw_intensity.info, show_axes=True, @@ -123,7 +124,6 @@ focalpoint=(0., -0.01, 0.02)) - ############################################################################### # TODO Can compare trans to the internal fsaverage-trans.fif as a test trans = mne.channels.compute_native_head_t(montage) From 28ff3820cdce535d00a53a44158ab67d932ee7b9 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Fri, 19 Mar 2021 09:42:51 +0000 Subject: [PATCH 13/44] Set montage with str in tutorial --- tutorials/io/plot_30_reading_fnirs_data.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tutorials/io/plot_30_reading_fnirs_data.py b/tutorials/io/plot_30_reading_fnirs_data.py index 681db0dfd29..23a2ee113e7 100644 --- a/tutorials/io/plot_30_reading_fnirs_data.py +++ b/tutorials/io/plot_30_reading_fnirs_data.py @@ -125,5 +125,8 @@ ############################################################################### -# TODO Can compare trans to the internal fsaverage-trans.fif as a test +# TODO Can compare trans to the internal fsaverage-trans.fif as a test: trans = mne.channels.compute_native_head_t(montage) + +# TODO Also works just with: +raw_intensity.set_montage('artinis-octamon') From 69bc74c54224a603136e4fec4c8608628d7a8f0e Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Sat, 20 Mar 2021 11:19:10 +1100 Subject: [PATCH 14/44] Add example of how to load unstructured data [ci skip] --- tutorials/io/plot_30_reading_fnirs_data.py | 134 +++++++++++++++------ 1 file changed, 96 insertions(+), 38 deletions(-) diff --git a/tutorials/io/plot_30_reading_fnirs_data.py b/tutorials/io/plot_30_reading_fnirs_data.py index 23a2ee113e7..99502486609 100644 --- a/tutorials/io/plot_30_reading_fnirs_data.py +++ b/tutorials/io/plot_30_reading_fnirs_data.py @@ -61,72 +61,130 @@ will also read the ``digaux`` data and create annotations for any triggers. -Storing of optode locations -=========================== - -NIRs devices consist of light sources and light detectors. -A channel is formed by source-detector pairs. -MNE stores the location of the channels, sources, and detectors. +""" # noqa:E501 +############################################################################### +# Loading legacy data in csv or tsv format +# ======================================== +# +# Many legacy fNIRS measurements are stored in csv and tsv formats. +# These formats are not officially supported in MNE as there is no +# standardisation of the file format - +# the naming and ordering of channels, the type and scaling of data, and +# specification of sensor positions varies between each vendor. +# Instead, we suggest that data is converted to the format approved by the +# Society for functional near-infrared spectroscopy called +# [SNIRF](https://github.com/fNIRS/snirf), the society provides converters +# to translate your data to SNIRF. +# However, due to the prevalence of these legacy files we provide +# a template example of how you may read data in t/csv formats. + +import numpy as np +import pandas as pd +import mne -.. warning:: Information about device light wavelength is stored in - channel names. Manual modification of channel names is not - recommended. - -""" # noqa:E501 +############################################################################### +# First, we generate an example csv file. +# This is only required for this example, this step would be skipped +# if you have actual data you wish to load. +# We simulate 16 channels with 100 samples of data and save this to a file +# called `fnirs.csv`. +pd.DataFrame(np.random.normal(size=(16, 100))).to_csv("fnirs.csv") ############################################################################### -# Fake some data -# -------------- -# -# Here we just create some fake data with the correct names for an -# artinis octamon system +# Next, we will load the example csv file. +# The metadata must be specified manually as the csv file does not contain +# information about channel names, types, sample rate etc. -import mne -from mne.channels import make_standard_montage -from mne.channels.tests.test_standard_montage import _simulate_artinis_octamon +data = pd.read_csv('fnirs.csv') + +# In MNE the naming of channels MUST follow this structure of +# `S#_D# type` or `S#_D# wavelength`, where # is replaced by the appropriate +# source and detector number. +ch_names = ['D1_S1 hbo', 'D1_S1 hbr', 'D1_S2 hbo', 'D1_S2 hbr', + 'D1_S3 hbo', 'D1_S3 hbr', 'D1_S4 hbo', 'D1_S4 hbr', + 'D2_S5 hbo', 'D2_S5 hbr', 'D2_S6 hbo', 'D2_S6 hbr', + 'D2_S7 hbo', 'D2_S7 hbr', 'D2_S8 hbo', 'D2_S8 hbr'] -raw_intensity = _simulate_artinis_octamon() +ch_types = ['hbo', 'hbr', 'hbo', 'hbr', + 'hbo', 'hbr', 'hbo', 'hbr', + 'hbo', 'hbr', 'hbo', 'hbr', + 'hbo', 'hbr', 'hbo', 'hbr'] +sfreq = 10. # Hz ############################################################################### -# Next we load the montage -# ------------------------------------------- -# -# And apply montage to data +# Finally, the data can be converted in to a MNE data structure. +# The metadata above is used to create an :class:`~mne.info` structure, +# and this is combined with the data to create +# an MNE :class:`~mne.io.Raw` object, for more details on how continuous +# data is stored in MNE see :ref:`tut-raw-class`. +# For a more extensive description of how to create MNE data structures from +# raw array data see :ref:`tut_creating_data_structures`. -montage = make_standard_montage("artinis-octamon") -raw_intensity.set_montage(montage) +info = mne.create_info(ch_names=ch_names, ch_types=ch_types, sfreq=sfreq) +raw = mne.io.RawArray(data, info, verbose=True) ############################################################################### -# View location of sensors over brain surface -# ------------------------------------------- +# Applying standard sensor locations to imported data +# --------------------------------------------------- # -# Here we validate +# Having information about optode locations may assist in your analysis. +# Beyond the general benefits this provides +# such as creating regions of interest, +# this is particularly important for fNIRS as information about the +# distance between optodes is required to convert the optical density data +# in to an estimate of the haemoglobin concentrations. +# MNE provides methods to load standard sensor configurations (montages) from +# some vendors, which is demonstrated below. +# However, many fNIRS researchers use custom optode montages, in this case +# you can generate your own `.elc` file +# (see [example file](https://github.com/mne-tools/mne-python/blob/main/mne/channels/data/montages/standard_1020.elc)) +# and load that instead. +# Below is an example of how to load the optode positions for a Artinis Octomon +# device. + +raw.set_montage('artinis-octamon') +# To load a custom montage use: +# raw.set_montage('/path/to/custom/montage.elc') + +# View the position of optodes in 2D to confirm the positions are correct. +raw.plot_sensors() + + +############################################################################### +# It is also possible to view the location of the sources (red), +# detectors (black), and channel (white lines and orange dots) locations +# in a 3D representation to validate the positions were loaded correctly. subjects_dir = mne.datasets.sample.data_path() + '/subjects' mne.datasets.fetch_fsaverage(subjects_dir=subjects_dir, verbose=True) fig = mne.viz.create_3d_figure(size=(800, 600), bgcolor='white') -fig = mne.viz.plot_alignment(raw_intensity.info, show_axes=True, - dig=True, mri_fiducials=True, +fig = mne.viz.plot_alignment(raw.info, show_axes=True, subject='fsaverage', coord_frame='mri', trans='fsaverage', surfaces=['brain', 'head'], fnirs=['channels', 'pairs', 'sources', 'detectors'], + dig=True, mri_fiducials=True, subjects_dir=subjects_dir, fig=fig) - -mne.viz.set_3d_view(figure=fig, azimuth=70, elevation=100, distance=0.4, +mne.viz.set_3d_view(figure=fig, azimuth=90, elevation=90, distance=0.4, focalpoint=(0., -0.01, 0.02)) ############################################################################### -# TODO Can compare trans to the internal fsaverage-trans.fif as a test: -trans = mne.channels.compute_native_head_t(montage) - -# TODO Also works just with: -raw_intensity.set_montage('artinis-octamon') +# Storing of optode locations +# =========================== +# +# NIRs devices consist of light sources and light detectors. +# A channel is formed by source-detector pairs. +# MNE stores the location of the channels, sources, and detectors. +# +# +# .. warning:: Information about device light wavelength is stored in +# channel names. Manual modification of channel names is not +# recommended. From eef41f339daebb86b43132d2d2be14649f211572 Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Sat, 20 Mar 2021 11:22:08 +1100 Subject: [PATCH 15/44] Typo [skip github] [azp skip] --- tutorials/io/plot_30_reading_fnirs_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/io/plot_30_reading_fnirs_data.py b/tutorials/io/plot_30_reading_fnirs_data.py index 99502486609..94a65536f49 100644 --- a/tutorials/io/plot_30_reading_fnirs_data.py +++ b/tutorials/io/plot_30_reading_fnirs_data.py @@ -180,7 +180,7 @@ # Storing of optode locations # =========================== # -# NIRs devices consist of light sources and light detectors. +# NIRS devices consist of light sources and light detectors. # A channel is formed by source-detector pairs. # MNE stores the location of the channels, sources, and detectors. # From 0a35722d65ff0958a518ed10abbcf249840e5a09 Mon Sep 17 00:00:00 2001 From: Robert Luke <748691+rob-luke@users.noreply.github.com> Date: Sat, 20 Mar 2021 11:55:36 +1100 Subject: [PATCH 16/44] More typos [skip github] [azp skip] --- tutorials/io/plot_30_reading_fnirs_data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tutorials/io/plot_30_reading_fnirs_data.py b/tutorials/io/plot_30_reading_fnirs_data.py index 94a65536f49..eeef0042f51 100644 --- a/tutorials/io/plot_30_reading_fnirs_data.py +++ b/tutorials/io/plot_30_reading_fnirs_data.py @@ -89,7 +89,7 @@ # This is only required for this example, this step would be skipped # if you have actual data you wish to load. # We simulate 16 channels with 100 samples of data and save this to a file -# called `fnirs.csv`. +# called "`fnirs.csv`". pd.DataFrame(np.random.normal(size=(16, 100))).to_csv("fnirs.csv") @@ -118,7 +118,7 @@ ############################################################################### # Finally, the data can be converted in to a MNE data structure. -# The metadata above is used to create an :class:`~mne.info` structure, +# The metadata above is used to create an Info structure, # and this is combined with the data to create # an MNE :class:`~mne.io.Raw` object, for more details on how continuous # data is stored in MNE see :ref:`tut-raw-class`. @@ -142,7 +142,7 @@ # MNE provides methods to load standard sensor configurations (montages) from # some vendors, which is demonstrated below. # However, many fNIRS researchers use custom optode montages, in this case -# you can generate your own `.elc` file +# you can generate your own "`.elc`" file # (see [example file](https://github.com/mne-tools/mne-python/blob/main/mne/channels/data/montages/standard_1020.elc)) # and load that instead. # Below is an example of how to load the optode positions for a Artinis Octomon From 03e6944073afab32b85c228a0d36afd2762f0157 Mon Sep 17 00:00:00 2001 From: Robert Luke <748691+rob-luke@users.noreply.github.com> Date: Sat, 20 Mar 2021 12:19:48 +1100 Subject: [PATCH 17/44] Typos [skip github] [azp skip] --- tutorials/io/plot_30_reading_fnirs_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tutorials/io/plot_30_reading_fnirs_data.py b/tutorials/io/plot_30_reading_fnirs_data.py index eeef0042f51..035e3cb2bc9 100644 --- a/tutorials/io/plot_30_reading_fnirs_data.py +++ b/tutorials/io/plot_30_reading_fnirs_data.py @@ -89,7 +89,7 @@ # This is only required for this example, this step would be skipped # if you have actual data you wish to load. # We simulate 16 channels with 100 samples of data and save this to a file -# called "`fnirs.csv`". +# called fnirs.csv. pd.DataFrame(np.random.normal(size=(16, 100))).to_csv("fnirs.csv") @@ -142,7 +142,7 @@ # MNE provides methods to load standard sensor configurations (montages) from # some vendors, which is demonstrated below. # However, many fNIRS researchers use custom optode montages, in this case -# you can generate your own "`.elc`" file +# you can generate your own .elc file # (see [example file](https://github.com/mne-tools/mne-python/blob/main/mne/channels/data/montages/standard_1020.elc)) # and load that instead. # Below is an example of how to load the optode positions for a Artinis Octomon From c04ad5cdf4f25e9e9f11ff732c8da6dddcc2da68 Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Sat, 20 Mar 2021 13:35:57 +1100 Subject: [PATCH 18/44] Fix linking [azp skip] [skip github] --- examples/visualization/plot_eeg_on_scalp.py | 3 ++ tutorials/io/plot_30_reading_fnirs_data.py | 49 ++++++++++++--------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/examples/visualization/plot_eeg_on_scalp.py b/examples/visualization/plot_eeg_on_scalp.py index 79d72dc5839..8047675013a 100644 --- a/examples/visualization/plot_eeg_on_scalp.py +++ b/examples/visualization/plot_eeg_on_scalp.py @@ -1,4 +1,7 @@ """ + +.. _ex-eeg-scalp: + ================================= Plotting EEG sensors on the scalp ================================= diff --git a/tutorials/io/plot_30_reading_fnirs_data.py b/tutorials/io/plot_30_reading_fnirs_data.py index 035e3cb2bc9..646288a9bc7 100644 --- a/tutorials/io/plot_30_reading_fnirs_data.py +++ b/tutorials/io/plot_30_reading_fnirs_data.py @@ -59,10 +59,10 @@ MNE will read either file type and extract the raw DC, AC, and Phase data. If triggers are sent using the ``digaux`` port of the recording hardware, MNE will also read the ``digaux`` data and create annotations for any triggers. - - """ # noqa:E501 +# sphinx_gallery_thumbnail_number = 2 + ############################################################################### # Loading legacy data in csv or tsv format # ======================================== @@ -74,8 +74,8 @@ # specification of sensor positions varies between each vendor. # Instead, we suggest that data is converted to the format approved by the # Society for functional near-infrared spectroscopy called -# [SNIRF](https://github.com/fNIRS/snirf), the society provides converters -# to translate your data to SNIRF. +# `SNIRF `_ +# to translate your data to the SNIRF format. # However, due to the prevalence of these legacy files we provide # a template example of how you may read data in t/csv formats. @@ -85,9 +85,8 @@ ############################################################################### -# First, we generate an example csv file. -# This is only required for this example, this step would be skipped -# if you have actual data you wish to load. +# First, we generate an example csv file which will then be loaded in to MNE. +# This step would be skipped if you have actual data you wish to load. # We simulate 16 channels with 100 samples of data and save this to a file # called fnirs.csv. @@ -102,8 +101,9 @@ data = pd.read_csv('fnirs.csv') # In MNE the naming of channels MUST follow this structure of -# `S#_D# type` or `S#_D# wavelength`, where # is replaced by the appropriate -# source and detector number. +# `S#_D# type` or `S#_D# wavelength`, where # is replaced +# by the appropriate source and detector number, type is +# either hbo or hbr, and wavelength is specified in nm. ch_names = ['D1_S1 hbo', 'D1_S1 hbr', 'D1_S2 hbo', 'D1_S2 hbr', 'D1_S3 hbo', 'D1_S3 hbr', 'D1_S4 hbo', 'D1_S4 hbr', 'D2_S5 hbo', 'D2_S5 hbr', 'D2_S6 hbo', 'D2_S6 hbr', @@ -117,10 +117,11 @@ ############################################################################### -# Finally, the data can be converted in to a MNE data structure. -# The metadata above is used to create an Info structure, +# Finally, the data can be converted in to an MNE data structure. +# The metadata above is used to create an :class:`mne.Info` data structure, # and this is combined with the data to create -# an MNE :class:`~mne.io.Raw` object, for more details on how continuous +# an MNE :class:`~mne.io.Raw` object. For more details on the info structure +# see :ref:`tut-info-class`, and for additional details on how continuous # data is stored in MNE see :ref:`tut-raw-class`. # For a more extensive description of how to create MNE data structures from # raw array data see :ref:`tut_creating_data_structures`. @@ -135,18 +136,22 @@ # # Having information about optode locations may assist in your analysis. # Beyond the general benefits this provides -# such as creating regions of interest, +# (e.g. creating regions of interest, etc), # this is particularly important for fNIRS as information about the # distance between optodes is required to convert the optical density data # in to an estimate of the haemoglobin concentrations. # MNE provides methods to load standard sensor configurations (montages) from -# some vendors, which is demonstrated below. -# However, many fNIRS researchers use custom optode montages, in this case -# you can generate your own .elc file -# (see [example file](https://github.com/mne-tools/mne-python/blob/main/mne/channels/data/montages/standard_1020.elc)) -# and load that instead. -# Below is an example of how to load the optode positions for a Artinis Octomon -# device. +# some vendors, and this is demonstrated below. +# Some handy tutorials for understanding sensor locations, coordinate systems, +# and how to store and view this information in MNE are: +# :ref:`tut-sensor-locations`, :ref:`plot_source_alignment`, and +# :ref:`ex-eeg-scalp`. + +# Below is an example of how to load the optode positions for an Artinis +# Octomon device. However, many fNIRS researchers use custom optode montages, +# in this case you can generate your own .elc file (see `example file +# `_) and load that instead. raw.set_montage('artinis-octamon') # To load a custom montage use: @@ -160,6 +165,8 @@ # It is also possible to view the location of the sources (red), # detectors (black), and channel (white lines and orange dots) locations # in a 3D representation to validate the positions were loaded correctly. +# The ficiduals are marked in blue, green and red. +# See :ref:`plot_source_alignment` for more details. subjects_dir = mne.datasets.sample.data_path() + '/subjects' mne.datasets.fetch_fsaverage(subjects_dir=subjects_dir, verbose=True) @@ -180,7 +187,7 @@ # Storing of optode locations # =========================== # -# NIRS devices consist of light sources and light detectors. +# fNIRS devices consist of light sources and light detectors. # A channel is formed by source-detector pairs. # MNE stores the location of the channels, sources, and detectors. # From 059ae2fbfe66b217b73efa96751b66a6a3e04ada Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Sat, 20 Mar 2021 13:52:58 +1100 Subject: [PATCH 19/44] Small doc tweaks [azp skip] [github skip] --- tutorials/io/plot_30_reading_fnirs_data.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tutorials/io/plot_30_reading_fnirs_data.py b/tutorials/io/plot_30_reading_fnirs_data.py index 646288a9bc7..938eac3e09f 100644 --- a/tutorials/io/plot_30_reading_fnirs_data.py +++ b/tutorials/io/plot_30_reading_fnirs_data.py @@ -17,6 +17,7 @@ NIRx recordings can be read in using :func:`mne.io.read_raw_nirx`. The NIRx device stores data directly to a directory with multiple file types, MNE extracts the appropriate information from each file. +MNE only supports NIRx files recorded with NIRStar version 15.0 and above. .. _import-snirf: @@ -74,8 +75,9 @@ # specification of sensor positions varies between each vendor. # Instead, we suggest that data is converted to the format approved by the # Society for functional near-infrared spectroscopy called -# `SNIRF `_ -# to translate your data to the SNIRF format. +# `SNIRF `_, +# they provide a number of tools to convert your legacy +# data to the SNIRF format. # However, due to the prevalence of these legacy files we provide # a template example of how you may read data in t/csv formats. @@ -146,7 +148,7 @@ # and how to store and view this information in MNE are: # :ref:`tut-sensor-locations`, :ref:`plot_source_alignment`, and # :ref:`ex-eeg-scalp`. - +# # Below is an example of how to load the optode positions for an Artinis # Octomon device. However, many fNIRS researchers use custom optode montages, # in this case you can generate your own .elc file (see `example file @@ -162,9 +164,9 @@ ############################################################################### -# It is also possible to view the location of the sources (red), -# detectors (black), and channel (white lines and orange dots) locations -# in a 3D representation to validate the positions were loaded correctly. +# To validate the positions were loaded correctly it is also possible +# to view the location of the sources (red), detectors (black), +# and channel (white lines and orange dots) locations in a 3D representation. # The ficiduals are marked in blue, green and red. # See :ref:`plot_source_alignment` for more details. From 7fc5db9797a1ff33f5d481f862515d66bf0a1875 Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Sat, 20 Mar 2021 14:03:14 +1100 Subject: [PATCH 20/44] Add tests. More needed --- .../data/montages/artinis-brite23.txt | 58 ------------------- .../data/montages/artinis-octamon.txt | 35 ----------- mne/channels/tests/test_standard_montage.py | 13 ++++- 3 files changed, 12 insertions(+), 94 deletions(-) delete mode 100644 mne/channels/data/montages/artinis-brite23.txt delete mode 100644 mne/channels/data/montages/artinis-octamon.txt diff --git a/mne/channels/data/montages/artinis-brite23.txt b/mne/channels/data/montages/artinis-brite23.txt deleted file mode 100644 index a47b4c3cad1..00000000000 --- a/mne/channels/data/montages/artinis-brite23.txt +++ /dev/null @@ -1,58 +0,0 @@ -MNI Coordinates - -Brain Atlas Coordinate System Projection Method -MNI ICBM 152 nonlinear asymmetric 2009a MNI Coordinate System Scalp Projection - -Fiducial x-Coordinate y-Coordinate z-Coordinate -Nz -4.62 82.33 -45.74 -Iz -4.90 -115.92 -31.92 -RPA 79.66 -18.72 -45.89 -LPA -81.41 -17.18 -45.56 -Cz 2.81 -12.59 96.67 - -Optode x-Coordinate y-Coordinate z-Coordinate -Rx1 65.18 27.28 35.31 -Rx2 48.62 59.71 22.68 -Rx3 18.95 72.41 38.32 -Rx4 -3.97 79.74 30.28 -Rx5 -25.96 72.19 35.16 -Rx6 -52.51 60.53 14.54 -Rx7 -66.37 32.04 31.08 - -Optode x-Coordinate y-Coordinate z-Coordinate -Tx1 76.10 -0.29 31.24 -Tx2 65.61 -0.26 56.15 -Tx3 64.93 42.43 8.29 -Tx4 43.32 46.36 50.77 -Tx5 21.58 82.45 1.06 -Tx6 -2.91 59.57 61.59 -Tx7 -29.62 79.35 2.38 -Tx8 -48.13 44.76 49.15 -Tx9 -67.68 43.26 -3.18 -Tx10 -65.37 4.89 56.36 -Tx11 -77.24 5.88 27.58 - -Channel x-Coordinate y-Coordinate z-Coordinate -Rx1-Tx1 70.71 14.91 34.96 -Rx1-Tx2 65.96 11.27 48.41 -Rx1-Tx3 65.53 36.16 21.42 -Rx1-Tx4 55.49 36.06 44.50 -Rx2-Tx3 57.64 52.09 13.64 -Rx2-Tx4 46.47 52.98 38.87 -Rx2-Tx5 36.77 72.78 13.84 -Rx3-Tx4 34.91 59.03 45.54 -Rx3-Tx5 20.83 78.65 22.34 -Rx3-Tx6 7.44 68.09 50.48 -Rx4-Tx5 8.37 83.62 17.02 -Rx4-Tx6 -3.95 71.80 45.98 -Rx4-Tx7 -16.94 82.98 14.76 -Rx5-Tx6 -18.12 67.26 48.34 -Rx5-Tx7 -29.46 77.35 17.29 -Rx5-Tx8 -38.28 58.97 44.13 -Rx6-Tx7 -43.67 70.05 8.04 -Rx6-Tx8 -51.14 53.07 33.96 -Rx6-Tx9 -61.17 52.21 6.64 -Rx7-Tx8 -57.77 39.15 40.61 -Rx7-Tx9 -68.49 37.56 14.92 -Rx7-Tx10 -67.44 19.36 42.90 -Rx7-Tx11 -73.47 16.23 31.14 diff --git a/mne/channels/data/montages/artinis-octamon.txt b/mne/channels/data/montages/artinis-octamon.txt deleted file mode 100644 index e594631be75..00000000000 --- a/mne/channels/data/montages/artinis-octamon.txt +++ /dev/null @@ -1,35 +0,0 @@ -MNI Coordinates - -Brain Atlas Coordinate System Projection Method -MNI ICBM 152 nonlinear asymmetric 2009a MNI Coordinate System Scalp Projection - -Fiducial x-Coordinate y-Coordinate z-Coordinate -Nz 0.96 83.56 -48.63 -Iz 0.33 -115.25 -34.65 -RPA 80.25 -19.67 -43.88 -LPA -82.58 -20.09 -43.10 -Cz 0.65 -12.86 96.78 - -Optode x-Coordinate y-Coordinate z-Coordinate -Rx1 47.77 65.28 7.28 -Rx2 -46.45 67.76 8.81 - -Optode x-Coordinate y-Coordinate z-Coordinate -Tx1 63.88 34.84 28.34 -Tx2 64.96 45.02 -10.31 -Tx3 22.07 74.86 31.03 -Tx4 17.84 84.96 -10.84 -Tx5 -10.81 77.96 32.10 -Tx6 -15.96 85.24 -7.41 -Tx7 -61.78 40.78 29.92 -Tx8 -65.28 48.14 -10.73 - -Channel x-Coordinate y-Coordinate z-Coordinate -Rx1-Tx1 57.00 50.56 19.33 -Rx1-Tx2 57.12 56.10 1.26 -Rx1-Tx3 38.41 70.37 19.56 -Rx1-Tx4 32.83 76.89 -0.91 -Rx2-Tx5 -31.45 75.44 21.04 -Rx2-Tx6 -33.04 77.66 0.59 -Rx2-Tx7 -56.11 54.37 19.49 -Rx2-Tx8 -55.73 60.39 -1.00 diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index cc84d0c91df..e9aa210f96c 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -8,7 +8,7 @@ import numpy as np -from numpy.testing import assert_allclose +from numpy.testing import assert_allclose, assert_array_almost_equal from mne.channels import make_standard_montage from mne.io._digitization import _get_dig_eeg, _get_fid_coords @@ -109,3 +109,14 @@ def test_artinis(): raw = _simulate_artinis_octamon() montage = make_standard_montage("artinis-octamon") raw.set_montage(montage) + # Check a known location + assert_array_almost_equal(raw.info["chs"][0]["loc"][:3], + [0.0616, 0.075398, 0.07347]) + assert_array_almost_equal(raw.info["chs"][8]["loc"][:3], + [-0.033875, 0.101276, 0.077291]) + assert_array_almost_equal(raw.info["chs"][12]["loc"][:3], + [-0.062749, 0.080417, 0.074884]) + # fNIRS has two identical channel locations for each measurement + # The 10th element encodes the wavelength, so it will differ. + assert_array_almost_equal(raw.info["chs"][0]["loc"][:9], + raw.info["chs"][1]["loc"][:9]) From 7e00eb5e955a3c2819e4aae2cd97a615ba8bdcbf Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Sat, 20 Mar 2021 14:08:15 +1100 Subject: [PATCH 21/44] More tests --- mne/channels/tests/test_standard_montage.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index e9aa210f96c..a719ba5a98a 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -8,7 +8,8 @@ import numpy as np -from numpy.testing import assert_allclose, assert_array_almost_equal +from numpy.testing import (assert_allclose, assert_array_almost_equal, + assert_raises) from mne.channels import make_standard_montage from mne.io._digitization import _get_dig_eeg, _get_fid_coords @@ -107,8 +108,13 @@ def _simulate_artinis_octamon(): def test_artinis(): raw = _simulate_artinis_octamon() + old_info = raw.info.copy() montage = make_standard_montage("artinis-octamon") raw.set_montage(montage) + # First check that the montage was actually modified + assert_raises(AssertionError, assert_array_almost_equal, + old_info["chs"][0]["loc"][:9], + raw.info["chs"][0]["loc"][:9]) # Check a known location assert_array_almost_equal(raw.info["chs"][0]["loc"][:3], [0.0616, 0.075398, 0.07347]) From a47743ee9a0d870506a882b788f7d4756d47bd84 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Sat, 20 Mar 2021 17:48:18 +0000 Subject: [PATCH 22/44] Update doc and tests --- mne/channels/montage.py | 6 +-- mne/channels/tests/test_standard_montage.py | 55 ++++++++++++--------- tutorials/io/plot_30_reading_fnirs_data.py | 46 ++++++++--------- 3 files changed, 55 insertions(+), 52 deletions(-) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index b2a44a3750a..41f21686208 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -733,7 +733,7 @@ def _set_montage_fnirs(info, montage): there is a source optode location, a detector optode location, and a channel midpoint that must be stored. """ - # Modify info['chs'][_]['loc'] in place + # Modify info['chs'][#]['loc'] in place num_ficiduals = len(montage.dig) - len(montage.ch_names) picks = _picks_to_idx(info, 'fnirs', exclude=[], allow_empty=True) for ch_idx in picks: @@ -893,7 +893,7 @@ def _backcompat_value(pos, ref_pos): for name, use in zip(info_names, info_names_use): _loc_view = info['chs'][info['ch_names'].index(name)]['loc'] - # XXX info['chs'][_]['loc'] modified in place + # XXX info['chs'][#]['loc'] modified in place _loc_view[:6] = _backcompat_value(ch_pos_use[use], eeg_ref_pos) del ch_pos_use @@ -937,7 +937,7 @@ def _backcompat_value(pos, ref_pos): # XXX info['dig'] modified in place info['dig'] = None for ch in info['chs']: - # XXX info['chs'][_]['loc'] modified in place + # XXX info['chs'][#]['loc'] modified in place ch['loc'] = np.full(12, np.nan) diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index a719ba5a98a..40df59fa8ab 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -7,13 +7,17 @@ import pytest import numpy as np +import os.path as op from numpy.testing import (assert_allclose, assert_array_almost_equal, assert_raises) +from mne import create_info, read_trans from mne.channels import make_standard_montage +from mne.io import RawArray from mne.io._digitization import _get_dig_eeg, _get_fid_coords from mne.channels.montage import get_builtin_montages, HEAD_SIZE_DEFAULT +from mne.channels import compute_native_head_t from mne.io.constants import FIFF @@ -78,39 +82,31 @@ def test_standard_superset(): def _simulate_artinis_octamon(): """ - Simulate artinis octamon channel data from nirx data. + Simulate artinis octamon channel data from numpy data. This is to test data that is imported with missing or incorrect montage info. This data can then be used to test the set_montage function. """ - import os.path as op - from mne.datasets import fnirs_motor - from mne.io import read_raw_nirx - fnirs_data_folder = fnirs_motor.data_path() - fnirs_cw_amplitude_dir = op.join(fnirs_data_folder, 'Participant-1') - raw = read_raw_nirx(fnirs_cw_amplitude_dir, preload=True) - mapping = {'S1_D2 760': 'S3_D1 760', 'S1_D2 850': 'S3_D1 850', - 'S1_D3 760': 'S4_D1 760', 'S1_D3 850': 'S4_D1 850', - 'S1_D9 760': 'S5_D2 760', 'S1_D9 850': 'S5_D2 850', - 'S2_D3 760': 'S6_D2 760', 'S2_D3 850': 'S6_D2 850', - 'S2_D4 760': 'S7_D2 760', 'S2_D4 850': 'S7_D2 850', - 'S2_D10 760': 'S8_D2 760', 'S2_D10 850': 'S8_D2 850'} - raw.rename_channels(mapping) - raw.pick(picks=["S1_D1 760", "S1_D1 850", - "S2_D1 760", "S2_D1 850", - "S3_D1 760", "S3_D1 850", - "S4_D1 760", "S4_D1 850", - "S5_D2 760", "S5_D2 850", - "S6_D2 760", "S6_D2 850", - "S7_D2 760", "S7_D2 850", - "S8_D2 760", "S8_D2 850"]) + data = np.random.normal(size=(16, 100)) + ch_names = ['D1_S1 hbo', 'D1_S1 hbr', 'D1_S2 hbo', 'D1_S2 hbr', + 'D1_S3 hbo', 'D1_S3 hbr', 'D1_S4 hbo', 'D1_S4 hbr', + 'D2_S5 hbo', 'D2_S5 hbr', 'D2_S6 hbo', 'D2_S6 hbr', + 'D2_S7 hbo', 'D2_S7 hbr', 'D2_S8 hbo', 'D2_S8 hbr'] + ch_types = ['hbo', 'hbr', 'hbo', 'hbr', + 'hbo', 'hbr', 'hbo', 'hbr', + 'hbo', 'hbr', 'hbo', 'hbr', + 'hbo', 'hbr', 'hbo', 'hbr'] + sfreq = 10. # Hz + info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=sfreq) + raw = RawArray(data, info, verbose=True) + return raw def test_artinis(): raw = _simulate_artinis_octamon() old_info = raw.info.copy() - montage = make_standard_montage("artinis-octamon") - raw.set_montage(montage) + montage_octamon = make_standard_montage("artinis-octamon") + raw.set_montage(montage_octamon) # First check that the montage was actually modified assert_raises(AssertionError, assert_array_almost_equal, old_info["chs"][0]["loc"][:9], @@ -126,3 +122,14 @@ def test_artinis(): # The 10th element encodes the wavelength, so it will differ. assert_array_almost_equal(raw.info["chs"][0]["loc"][:9], raw.info["chs"][1]["loc"][:9]) + # Compare OctaMon and Brite23 to fsaverage + trans_octamon = compute_native_head_t(montage_octamon) + montage_brite = make_standard_montage("artinis-brite23") + trans_brite = compute_native_head_t(montage_brite) + fif = op.join(op.dirname(__file__), '..', '..', 'data', 'fsaverage', + 'fsaverage-trans.fif') + fsaverage = read_trans(fif) + assert_array_almost_equal(list(trans_octamon.values())[2], + list(fsaverage.values())[2]) + assert_array_almost_equal(list(trans_brite.values())[2], + list(fsaverage.values())[2]) diff --git a/tutorials/io/plot_30_reading_fnirs_data.py b/tutorials/io/plot_30_reading_fnirs_data.py index 938eac3e09f..c30a545bee1 100644 --- a/tutorials/io/plot_30_reading_fnirs_data.py +++ b/tutorials/io/plot_30_reading_fnirs_data.py @@ -60,31 +60,29 @@ MNE will read either file type and extract the raw DC, AC, and Phase data. If triggers are sent using the ``digaux`` port of the recording hardware, MNE will also read the ``digaux`` data and create annotations for any triggers. -""" # noqa:E501 - -# sphinx_gallery_thumbnail_number = 2 -############################################################################### -# Loading legacy data in csv or tsv format -# ======================================== -# -# Many legacy fNIRS measurements are stored in csv and tsv formats. -# These formats are not officially supported in MNE as there is no -# standardisation of the file format - -# the naming and ordering of channels, the type and scaling of data, and -# specification of sensor positions varies between each vendor. -# Instead, we suggest that data is converted to the format approved by the -# Society for functional near-infrared spectroscopy called -# `SNIRF `_, -# they provide a number of tools to convert your legacy -# data to the SNIRF format. -# However, due to the prevalence of these legacy files we provide -# a template example of how you may read data in t/csv formats. +Loading legacy data in csv or tsv format +======================================== + +Many legacy fNIRS measurements are stored in csv and tsv formats. +These formats are not officially supported in MNE as there is no +standardisation of the file format - +the naming and ordering of channels, the type and scaling of data, and +specification of sensor positions varies between each vendor. +Instead, we suggest that data is converted to the format approved by the +Society for functional Near-Infrared Spectroscopy called +`SNIRF `_, +they provide a number of tools to convert your legacy +data to the SNIRF format. +However, due to the prevalence of these legacy files we provide +a template example of how you may read data in t/csv formats. +""" # noqa:E501 import numpy as np import pandas as pd import mne +# sphinx_gallery_thumbnail_number = 2 ############################################################################### # First, we generate an example csv file which will then be loaded in to MNE. @@ -106,11 +104,11 @@ # `S#_D# type` or `S#_D# wavelength`, where # is replaced # by the appropriate source and detector number, type is # either hbo or hbr, and wavelength is specified in nm. + ch_names = ['D1_S1 hbo', 'D1_S1 hbr', 'D1_S2 hbo', 'D1_S2 hbr', 'D1_S3 hbo', 'D1_S3 hbr', 'D1_S4 hbo', 'D1_S4 hbr', 'D2_S5 hbo', 'D2_S5 hbr', 'D2_S6 hbo', 'D2_S6 hbr', 'D2_S7 hbo', 'D2_S7 hbr', 'D2_S8 hbo', 'D2_S8 hbr'] - ch_types = ['hbo', 'hbr', 'hbo', 'hbr', 'hbo', 'hbr', 'hbo', 'hbr', 'hbo', 'hbr', 'hbo', 'hbr', @@ -150,14 +148,12 @@ # :ref:`ex-eeg-scalp`. # # Below is an example of how to load the optode positions for an Artinis -# Octomon device. However, many fNIRS researchers use custom optode montages, +# OctaMon device. However, many fNIRS researchers use custom optode montages, # in this case you can generate your own .elc file (see `example file # `_) and load that instead. raw.set_montage('artinis-octamon') -# To load a custom montage use: -# raw.set_montage('/path/to/custom/montage.elc') # View the position of optodes in 2D to confirm the positions are correct. raw.plot_sensors() @@ -165,13 +161,13 @@ ############################################################################### # To validate the positions were loaded correctly it is also possible -# to view the location of the sources (red), detectors (black), +# to view the location of the sources (black), detectors (red), # and channel (white lines and orange dots) locations in a 3D representation. # The ficiduals are marked in blue, green and red. # See :ref:`plot_source_alignment` for more details. subjects_dir = mne.datasets.sample.data_path() + '/subjects' -mne.datasets.fetch_fsaverage(subjects_dir=subjects_dir, verbose=True) +mne.datasets.fetch_fsaverage(subjects_dir=subjects_dir) fig = mne.viz.create_3d_figure(size=(800, 600), bgcolor='white') fig = mne.viz.plot_alignment(raw.info, show_axes=True, From 4675c642935bd024e4dfe87734b7fc4b41497199 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Sat, 20 Mar 2021 19:12:55 +0000 Subject: [PATCH 23/44] Fix pydocstyle --- mne/channels/montage.py | 12 +++++++----- mne/channels/tests/test_standard_montage.py | 7 ++++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index 41f21686208..e0449ed5886 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -727,11 +727,13 @@ def _get_montage_in_head(montage): def _set_montage_fnirs(info, montage): - """ - Sets the montage for fNIRS data. This needs to be different to electrodes - as each channel has three coordinates that need to be set. For each channel - there is a source optode location, a detector optode location, - and a channel midpoint that must be stored. + """Set the montage for fNIRS data. + + This needs to be different to electrodes as each channel has three + coordinates that need to be set. For each channel there is a source optode + location, a detector optode location, and a channel midpoint that must be + stored. This function modifies info['chs'][#]['loc'] and info['dig'] in + place. """ # Modify info['chs'][#]['loc'] in place num_ficiduals = len(montage.dig) - len(montage.ch_names) diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index 40df59fa8ab..bb00734fcf7 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -81,8 +81,8 @@ def test_standard_superset(): def _simulate_artinis_octamon(): - """ - Simulate artinis octamon channel data from numpy data. + """Simulate artinis OctaMon channel data from numpy data. + This is to test data that is imported with missing or incorrect montage info. This data can then be used to test the set_montage function. """ @@ -102,7 +102,8 @@ def _simulate_artinis_octamon(): return raw -def test_artinis(): +def test_set_montage_artinis(): + """Test that OctaMon and Brite 23 montages are set properly.""" raw = _simulate_artinis_octamon() old_info = raw.info.copy() montage_octamon = make_standard_montage("artinis-octamon") From 4fb7463296e99f24c771d0b6a210ddfef24f362f Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Sun, 21 Mar 2021 10:47:40 +1100 Subject: [PATCH 24/44] Add more warning and limitations --- mne/channels/tests/test_standard_montage.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index bb00734fcf7..c402133ef97 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -130,7 +130,10 @@ def test_set_montage_artinis(): fif = op.join(op.dirname(__file__), '..', '..', 'data', 'fsaverage', 'fsaverage-trans.fif') fsaverage = read_trans(fif) - assert_array_almost_equal(list(trans_octamon.values())[2], - list(fsaverage.values())[2]) - assert_array_almost_equal(list(trans_brite.values())[2], - list(fsaverage.values())[2]) + # assert_array_almost_equal(list(trans_octamon.values())[2], + # list(fsaverage.values())[2]) + # assert_array_almost_equal(list(trans_brite.values())[2], + # list(fsaverage.values())[2]) + + raw.info['bads'] = raw.ch_names[3] + raw.interpolate_bads() From 27b812372a4b6855a921ccd5854641dcfa65ab77 Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Sun, 21 Mar 2021 11:03:53 +1100 Subject: [PATCH 25/44] added wrong file --- mne/channels/tests/test_standard_montage.py | 10 ++++------ tutorials/io/plot_30_reading_fnirs_data.py | 21 ++++++++++++++++++--- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index c402133ef97..7f53bd233c4 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -130,10 +130,8 @@ def test_set_montage_artinis(): fif = op.join(op.dirname(__file__), '..', '..', 'data', 'fsaverage', 'fsaverage-trans.fif') fsaverage = read_trans(fif) - # assert_array_almost_equal(list(trans_octamon.values())[2], - # list(fsaverage.values())[2]) - # assert_array_almost_equal(list(trans_brite.values())[2], - # list(fsaverage.values())[2]) + assert_array_almost_equal(list(trans_octamon.values())[2], + list(fsaverage.values())[2]) + assert_array_almost_equal(list(trans_brite.values())[2], + list(fsaverage.values())[2]) - raw.info['bads'] = raw.ch_names[3] - raw.interpolate_bads() diff --git a/tutorials/io/plot_30_reading_fnirs_data.py b/tutorials/io/plot_30_reading_fnirs_data.py index c30a545bee1..f3adfe145f8 100644 --- a/tutorials/io/plot_30_reading_fnirs_data.py +++ b/tutorials/io/plot_30_reading_fnirs_data.py @@ -61,9 +61,20 @@ If triggers are sent using the ``digaux`` port of the recording hardware, MNE will also read the ``digaux`` data and create annotations for any triggers. + Loading legacy data in csv or tsv format ======================================== +.. warning:: This method is not supported. You should convert your data + to the `SNIRF `_ + format using the tools provided by the Society + for functional Near-Infrared Spectroscopy, and then load it + using :func:`mne.io.read_raw_snirf`. + + This method will only work for data that has already been + converted to oxyhaemoglobin and deoxyhaemoglobin. It will not work + with raw intensity data or optical density data. + Many legacy fNIRS measurements are stored in csv and tsv formats. These formats are not officially supported in MNE as there is no standardisation of the file format - @@ -94,6 +105,10 @@ ############################################################################### +# +# .. warning:: You must ensure that the channel naming structure follows +# the MNE format of `S#_D# type`. This is further described below. +# # Next, we will load the example csv file. # The metadata must be specified manually as the csv file does not contain # information about channel names, types, sample rate etc. @@ -101,9 +116,9 @@ data = pd.read_csv('fnirs.csv') # In MNE the naming of channels MUST follow this structure of -# `S#_D# type` or `S#_D# wavelength`, where # is replaced -# by the appropriate source and detector number, type is -# either hbo or hbr, and wavelength is specified in nm. +# `S#_D# type` where # is replaced +# by the appropriate source and detector number and type is +# either hbo or hbr. ch_names = ['D1_S1 hbo', 'D1_S1 hbr', 'D1_S2 hbo', 'D1_S2 hbr', 'D1_S3 hbo', 'D1_S3 hbr', 'D1_S4 hbo', 'D1_S4 hbr', From d60f88eab6f93c982cdce7873c00680c03103eb2 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Sun, 21 Mar 2021 00:12:41 +0000 Subject: [PATCH 26/44] Fix reverted source and detector --- mne/channels/montage.py | 2 +- mne/channels/tests/test_standard_montage.py | 19 ++++++++----------- tutorials/io/plot_30_reading_fnirs_data.py | 10 +++++----- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index e0449ed5886..862633b7e57 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -740,7 +740,7 @@ def _set_montage_fnirs(info, montage): picks = _picks_to_idx(info, 'fnirs', exclude=[], allow_empty=True) for ch_idx in picks: ch = info['ch_names'][ch_idx] - source, detector = ch.split(' ')[0].split('_') + detector, source = sorted(ch.split(' ')[0].split('_')) source_pos = montage.dig[montage.ch_names.index(source) + num_ficiduals]['r'] detector_pos = montage.dig[montage.ch_names.index(detector) diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index c402133ef97..5e069d2d611 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -87,10 +87,10 @@ def _simulate_artinis_octamon(): info. This data can then be used to test the set_montage function. """ data = np.random.normal(size=(16, 100)) - ch_names = ['D1_S1 hbo', 'D1_S1 hbr', 'D1_S2 hbo', 'D1_S2 hbr', - 'D1_S3 hbo', 'D1_S3 hbr', 'D1_S4 hbo', 'D1_S4 hbr', - 'D2_S5 hbo', 'D2_S5 hbr', 'D2_S6 hbo', 'D2_S6 hbr', - 'D2_S7 hbo', 'D2_S7 hbr', 'D2_S8 hbo', 'D2_S8 hbr'] + ch_names = ['S1_D1 hbo', 'S1_D1 hbr', 'S2_D1 hbo', 'S2_D1 hbr', + 'S3_D1 hbo', 'S3_D1 hbr', 'S4_D1 hbo', 'S4_D1 hbr', + 'S5_D2 hbo', 'S5_D2 hbr', 'S6_D2 hbo', 'S6_D2 hbr', + 'S7_D2 hbo', 'S7_D2 hbr', 'S8_D2 hbo', 'S8_D2 hbr'] ch_types = ['hbo', 'hbr', 'hbo', 'hbr', 'hbo', 'hbr', 'hbo', 'hbr', 'hbo', 'hbr', 'hbo', 'hbr', @@ -130,10 +130,7 @@ def test_set_montage_artinis(): fif = op.join(op.dirname(__file__), '..', '..', 'data', 'fsaverage', 'fsaverage-trans.fif') fsaverage = read_trans(fif) - # assert_array_almost_equal(list(trans_octamon.values())[2], - # list(fsaverage.values())[2]) - # assert_array_almost_equal(list(trans_brite.values())[2], - # list(fsaverage.values())[2]) - - raw.info['bads'] = raw.ch_names[3] - raw.interpolate_bads() + assert_array_almost_equal(list(trans_octamon.values())[2], + list(fsaverage.values())[2]) + assert_array_almost_equal(list(trans_brite.values())[2], + list(fsaverage.values())[2]) diff --git a/tutorials/io/plot_30_reading_fnirs_data.py b/tutorials/io/plot_30_reading_fnirs_data.py index c30a545bee1..08b5d68530a 100644 --- a/tutorials/io/plot_30_reading_fnirs_data.py +++ b/tutorials/io/plot_30_reading_fnirs_data.py @@ -105,10 +105,10 @@ # by the appropriate source and detector number, type is # either hbo or hbr, and wavelength is specified in nm. -ch_names = ['D1_S1 hbo', 'D1_S1 hbr', 'D1_S2 hbo', 'D1_S2 hbr', - 'D1_S3 hbo', 'D1_S3 hbr', 'D1_S4 hbo', 'D1_S4 hbr', - 'D2_S5 hbo', 'D2_S5 hbr', 'D2_S6 hbo', 'D2_S6 hbr', - 'D2_S7 hbo', 'D2_S7 hbr', 'D2_S8 hbo', 'D2_S8 hbr'] +ch_names = ['S1_D1 hbo', 'S1_D1 hbr', 'S2_D1 hbo', 'S2_D1 hbr', + 'S3_D1 hbo', 'S3_D1 hbr', 'S4_D1 hbo', 'S4_D1 hbr', + 'S5_D2 hbo', 'S5_D2 hbr', 'S6_D2 hbo', 'S6_D2 hbr', + 'S7_D2 hbo', 'S7_D2 hbr', 'S8_D2 hbo', 'S8_D2 hbr'] ch_types = ['hbo', 'hbr', 'hbo', 'hbr', 'hbo', 'hbr', 'hbo', 'hbr', 'hbo', 'hbr', 'hbo', 'hbr', @@ -161,7 +161,7 @@ ############################################################################### # To validate the positions were loaded correctly it is also possible -# to view the location of the sources (black), detectors (red), +# to view the location of the sources (red), detectors (black), # and channel (white lines and orange dots) locations in a 3D representation. # The ficiduals are marked in blue, green and red. # See :ref:`plot_source_alignment` for more details. From b7cfe66c461ed4aa3a04a0f3416df651215cc0fc Mon Sep 17 00:00:00 2001 From: Robert Luke <748691+rob-luke@users.noreply.github.com> Date: Sun, 21 Mar 2021 11:20:54 +1100 Subject: [PATCH 27/44] Typo --- tutorials/io/plot_30_reading_fnirs_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/io/plot_30_reading_fnirs_data.py b/tutorials/io/plot_30_reading_fnirs_data.py index 5cd3822a234..bb2909f97f5 100644 --- a/tutorials/io/plot_30_reading_fnirs_data.py +++ b/tutorials/io/plot_30_reading_fnirs_data.py @@ -107,7 +107,7 @@ ############################################################################### # # .. warning:: You must ensure that the channel naming structure follows -# the MNE format of `S#_D# type`. This is further described below. +# the MNE format of S#_D# type. This is further described below. # # Next, we will load the example csv file. # The metadata must be specified manually as the csv file does not contain From 9a4db5305c3685780b0dc686bc48665470bff52d Mon Sep 17 00:00:00 2001 From: Robert Luke Date: Sun, 21 Mar 2021 12:08:08 +1100 Subject: [PATCH 28/44] More warnings --- tutorials/io/plot_30_reading_fnirs_data.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tutorials/io/plot_30_reading_fnirs_data.py b/tutorials/io/plot_30_reading_fnirs_data.py index bb2909f97f5..85002a80d9f 100644 --- a/tutorials/io/plot_30_reading_fnirs_data.py +++ b/tutorials/io/plot_30_reading_fnirs_data.py @@ -107,7 +107,16 @@ ############################################################################### # # .. warning:: You must ensure that the channel naming structure follows -# the MNE format of S#_D# type. This is further described below. +# the MNE format of S#_D# type. +# The channels must be ordered in pairs haemoglobin pairs, +# such that for a single channel all the types are in subsequent +# indices. The type order must be hbo then hbr. +# The data below is already in the correct order and may be +# used as a template for how data must be stored. +# If the order that your data is stored is different to the +# mandatory formatting, then you must first read the data with +# channel naming according to the data structure, then reorder +# the channels to match the required format. # # Next, we will load the example csv file. # The metadata must be specified manually as the csv file does not contain From e4ed607a2ce728394c8551351ab965f485ba3acc Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Sun, 21 Mar 2021 10:00:46 +0000 Subject: [PATCH 29/44] Check fNIRS ch_names and add wavelength --- mne/channels/montage.py | 9 +++++++-- mne/io/meas_info.py | 28 +++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index 862633b7e57..ef5958acaa6 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -738,9 +738,13 @@ def _set_montage_fnirs(info, montage): # Modify info['chs'][#]['loc'] in place num_ficiduals = len(montage.dig) - len(montage.ch_names) picks = _picks_to_idx(info, 'fnirs', exclude=[], allow_empty=True) + info._check_nirs_ch_names() for ch_idx in picks: - ch = info['ch_names'][ch_idx] - detector, source = sorted(ch.split(' ')[0].split('_')) + ch = info['chs'][ch_idx]['ch_name'] + name, suffix = ch.split(' ') + if suffix not in ['hbo', 'hbr']: + info['chs'][ch_idx]['loc'][9] = int(suffix) + source, detector = name.split('_') source_pos = montage.dig[montage.ch_names.index(source) + num_ficiduals]['r'] detector_pos = montage.dig[montage.ch_names.index(detector) @@ -931,6 +935,7 @@ def _backcompat_value(pos, ref_pos): # XXX info['dev_head_t'] modified in place info['dev_head_t'] = Transform('meg', 'head', mnt_head.dev_head_t) + # Handle fNIRS with source, detector and channel fnirs_picks = _picks_to_idx(info, 'fnirs', allow_empty=True) if len(fnirs_picks) > 0: info = _set_montage_fnirs(info, mnt_head) diff --git a/mne/io/meas_info.py b/mne/io/meas_info.py index b020d26a3e3..dc816cfbed8 100644 --- a/mne/io/meas_info.py +++ b/mne/io/meas_info.py @@ -14,10 +14,13 @@ import operator from textwrap import shorten +import re as re + import numpy as np from .pick import (channel_type, pick_channels, pick_info, - get_channel_type_constants, pick_types) + get_channel_type_constants, pick_types, + _picks_to_idx, _get_channel_types) from .constants import FIFF, _coord_frame_named from .open import fiff_open from .tree import dir_tree_find @@ -774,6 +777,29 @@ def _check_consistency(self, prepend_error=''): warn('the "filename" key is misleading ' 'and info should not have it') + def _check_nirs_ch_names(self): + picks = _picks_to_idx(self, 'fnirs', exclude=[], allow_empty=True) + ch_types = _get_channel_types(self, picks=picks) + for idx in picks: + ch_name = self['chs'][idx]['ch_name'] + if ch_name != self['ch_names'][idx]: + raise RuntimeError( + 'Bad info: info["chs"][%d]["ch_name"] not matching ' + 'info["ch_names"][%d]' % (idx, idx)) + name, suffix = ch_name.split(' ') + if suffix != ch_types[idx]: + try: + int(suffix) + except ValueError: + raise RuntimeError( + 'Bad info: info["chs"][%d]["ch_name"] has an invalid ' + 'suffix %s' % (idx, suffix)) + s, d = name.split('_') + if not re.match('^S[0-9]+$', s) or not re.match('^D[0-9]+$', d): + raise RuntimeError( + 'Bad info: info["chs"][%d]["ch_name"] has an invalid ' + 'name %s, should follow the S#_D# format' % (idx, name)) + def _update_redundant(self): """Update the redundant entries.""" self['ch_names'] = [ch['ch_name'] for ch in self['chs']] From e3a6c7bc988d73f718287b2c105e2f7c62cb295e Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Fri, 9 Apr 2021 10:38:10 +0100 Subject: [PATCH 30/44] No modification of wavelength --- mne/channels/montage.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index 042453fb66b..1927b06198d 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -741,10 +741,7 @@ def _set_montage_fnirs(info, montage): info._check_nirs_ch_names() for ch_idx in picks: ch = info['chs'][ch_idx]['ch_name'] - name, suffix = ch.split(' ') - if suffix not in ['hbo', 'hbr']: - info['chs'][ch_idx]['loc'][9] = int(suffix) - source, detector = name.split('_') + source, detector = ch.split(' ')[0].split('_') source_pos = montage.dig[montage.ch_names.index(source) + num_ficiduals]['r'] detector_pos = montage.dig[montage.ch_names.index(detector) From 28e2a500b76e88821890134fb139b239c200603f Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Sun, 11 Apr 2021 17:24:52 +0100 Subject: [PATCH 31/44] Test Brite, intensity, OD --- mne/channels/tests/test_standard_montage.py | 130 +++++++++++++++----- 1 file changed, 99 insertions(+), 31 deletions(-) diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index 5e069d2d611..fed801adaa7 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -19,6 +19,7 @@ from mne.channels.montage import get_builtin_montages, HEAD_SIZE_DEFAULT from mne.channels import compute_native_head_t from mne.io.constants import FIFF +from mne.preprocessing.nirs import optical_density, beer_lambert_law @pytest.mark.parametrize('kind', get_builtin_montages()) @@ -29,7 +30,10 @@ def test_standard_montages_have_fids(kind): for k, v in fids.items(): assert v is not None, k for d in montage.dig: - assert d['coord_frame'] == FIFF.FIFFV_COORD_UNKNOWN + if kind == 'artinis-octamon' or kind == 'artinis-brite23': + assert d['coord_frame'] == FIFF.FIFFV_COORD_MRI + else: + assert d['coord_frame'] == FIFF.FIFFV_COORD_UNKNOWN def test_standard_montage_errors(): @@ -86,15 +90,41 @@ def _simulate_artinis_octamon(): This is to test data that is imported with missing or incorrect montage info. This data can then be used to test the set_montage function. """ - data = np.random.normal(size=(16, 100)) - ch_names = ['S1_D1 hbo', 'S1_D1 hbr', 'S2_D1 hbo', 'S2_D1 hbr', - 'S3_D1 hbo', 'S3_D1 hbr', 'S4_D1 hbo', 'S4_D1 hbr', - 'S5_D2 hbo', 'S5_D2 hbr', 'S6_D2 hbo', 'S6_D2 hbr', - 'S7_D2 hbo', 'S7_D2 hbr', 'S8_D2 hbo', 'S8_D2 hbr'] - ch_types = ['hbo', 'hbr', 'hbo', 'hbr', - 'hbo', 'hbr', 'hbo', 'hbr', - 'hbo', 'hbr', 'hbo', 'hbr', - 'hbo', 'hbr', 'hbo', 'hbr'] + np.random.seed(42) + data = np.absolute(np.random.normal(size=(16, 100))) + ch_names = ['S1_D1 760', 'S1_D1 850', 'S2_D1 760', 'S2_D1 850', + 'S3_D1 760', 'S3_D1 850', 'S4_D1 760', 'S4_D1 850', + 'S5_D2 760', 'S5_D2 850', 'S6_D2 760', 'S6_D2 850', + 'S7_D2 760', 'S7_D2 850', 'S8_D2 760', 'S8_D2 850'] + ch_types = ['fnirs_cw_amplitude' for _ in ch_names] + sfreq = 10. # Hz + info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=sfreq) + for i, ch_name in enumerate(ch_names): + info['chs'][i]['loc'][9] = int(ch_name.split(' ')[1]) + raw = RawArray(data, info, verbose=True) + + return raw + + +def _simulate_artinis_brite23(): + """Simulate artinis Brite 23 channel data from numpy data. + + This is to test data that is imported with missing or incorrect montage + info. This data can then be used to test the set_montage function. + """ + np.random.seed(0) + data = np.random.normal(size=(46, 100)) + sd_names = ['S1_D1', 'S2_D1', 'S3_D1', 'S4_D1', 'S3_D2', 'S4_D2', 'S5_D2', + 'S4_D3', 'S5_D3', 'S6_D3', 'S5_D4', 'S6_D4', 'S7_D4', 'S6_D5', + 'S7_D5', 'S8_D5', 'S7_D6', 'S8_D6', 'S9_D6', 'S8_D7', 'S9_D7', + 'S10_D7', 'S11_D7'] + ch_names = [] + ch_types = [] + for name in sd_names: + ch_names.append(name + ' hbo') + ch_types.append('hbo') + ch_names.append(name + ' hbr') + ch_types.append('hbr') sfreq = 10. # Hz info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=sfreq) raw = RawArray(data, info, verbose=True) @@ -104,29 +134,11 @@ def _simulate_artinis_octamon(): def test_set_montage_artinis(): """Test that OctaMon and Brite 23 montages are set properly.""" - raw = _simulate_artinis_octamon() - old_info = raw.info.copy() - montage_octamon = make_standard_montage("artinis-octamon") - raw.set_montage(montage_octamon) - # First check that the montage was actually modified - assert_raises(AssertionError, assert_array_almost_equal, - old_info["chs"][0]["loc"][:9], - raw.info["chs"][0]["loc"][:9]) - # Check a known location - assert_array_almost_equal(raw.info["chs"][0]["loc"][:3], - [0.0616, 0.075398, 0.07347]) - assert_array_almost_equal(raw.info["chs"][8]["loc"][:3], - [-0.033875, 0.101276, 0.077291]) - assert_array_almost_equal(raw.info["chs"][12]["loc"][:3], - [-0.062749, 0.080417, 0.074884]) - # fNIRS has two identical channel locations for each measurement - # The 10th element encodes the wavelength, so it will differ. - assert_array_almost_equal(raw.info["chs"][0]["loc"][:9], - raw.info["chs"][1]["loc"][:9]) # Compare OctaMon and Brite23 to fsaverage + montage_octamon = make_standard_montage('artinis-octamon') trans_octamon = compute_native_head_t(montage_octamon) - montage_brite = make_standard_montage("artinis-brite23") - trans_brite = compute_native_head_t(montage_brite) + montage_brite23 = make_standard_montage('artinis-brite23') + trans_brite = compute_native_head_t(montage_brite23) fif = op.join(op.dirname(__file__), '..', '..', 'data', 'fsaverage', 'fsaverage-trans.fif') fsaverage = read_trans(fif) @@ -134,3 +146,59 @@ def test_set_montage_artinis(): list(fsaverage.values())[2]) assert_array_almost_equal(list(trans_brite.values())[2], list(fsaverage.values())[2]) + + # Test OctaMon montage + raw = _simulate_artinis_octamon() + raw_od = optical_density(raw) + old_info = raw.info.copy() + old_info_od = raw_od.info.copy() + raw.set_montage(montage_octamon) + raw_od.set_montage(montage_octamon) + raw_hb = beer_lambert_law(raw_od) # montage needed for Beer Lambert law + # Check that the montage was actually modified + assert_raises(AssertionError, assert_array_almost_equal, + old_info['chs'][0]['loc'][:9], + raw.info['chs'][0]['loc'][:9]) + assert_raises(AssertionError, assert_array_almost_equal, + old_info_od['chs'][0]['loc'][:9], + raw_od.info['chs'][0]['loc'][:9]) + + # Check a known location + assert_array_almost_equal(raw.info['chs'][0]['loc'][:3], + [0.0616, 0.075398, 0.07347]) + assert_array_almost_equal(raw.info['chs'][8]['loc'][:3], + [-0.033875, 0.101276, 0.077291]) + assert_array_almost_equal(raw.info['chs'][12]['loc'][:3], + [-0.062749, 0.080417, 0.074884]) + assert_array_almost_equal(raw_od.info['chs'][12]['loc'][:3], + [-0.062749, 0.080417, 0.074884]) + assert_array_almost_equal(raw_hb.info['chs'][12]['loc'][:3], + [-0.062749, 0.080417, 0.074884]) + # Check that locations are identical for a pair of channels (all elements + # except the 10th which is the wavelength if not hbo and hbr type) + assert_array_almost_equal(raw.info['chs'][0]['loc'][:9], + raw.info['chs'][1]['loc'][:9]) + assert_array_almost_equal(raw_od.info['chs'][0]['loc'][:9], + raw_od.info['chs'][1]['loc'][:9]) + assert_array_almost_equal(raw_hb.info['chs'][0]['loc'][:9], + raw_hb.info['chs'][1]['loc'][:9]) + + # Test Brite 23 montage + raw = _simulate_artinis_brite23() + old_info = raw.info.copy() + raw.set_montage(montage_brite23) + # Check that the montage was actually modified + assert_raises(AssertionError, assert_array_almost_equal, + old_info['chs'][0]['loc'][:9], + raw.info['chs'][0]['loc'][:9]) + # Check a known location + assert_array_almost_equal(raw.info['chs'][0]['loc'][:3], + [0.085583, 0.036275, 0.089426]) + assert_array_almost_equal(raw.info['chs'][8]['loc'][:3], + [0.069555, 0.078579, 0.069305]) + assert_array_almost_equal(raw.info['chs'][12]['loc'][:3], + [0.044861, 0.100952, 0.065175]) + # Check that locations are identical for a pair of channels (all elements + # except the 10th which is the wavelength if not hbo and hbr type) + assert_array_almost_equal(raw.info['chs'][0]['loc'][:9], + raw.info['chs'][1]['loc'][:9]) From 4d4a80c0ccc31bbae2c381c29b462bf5e10f647a Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Sun, 11 Apr 2021 19:47:55 +0100 Subject: [PATCH 32/44] Use _check_channels_ordered with new API --- mne/channels/montage.py | 9 ++++++++- mne/io/meas_info.py | 28 +--------------------------- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index 1927b06198d..eef0d73c114 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -735,10 +735,17 @@ def _set_montage_fnirs(info, montage): stored. This function modifies info['chs'][#]['loc'] and info['dig'] in place. """ + from ..preprocessing.nirs import (_channel_frequencies, + _channel_chromophore, + _check_channels_ordered) # Modify info['chs'][#]['loc'] in place num_ficiduals = len(montage.dig) - len(montage.ch_names) picks = _picks_to_idx(info, 'fnirs', exclude=[], allow_empty=True) - info._check_nirs_ch_names() + freqs = np.unique(_channel_frequencies(info)) + if freqs.size > 0: + _check_channels_ordered(info, freqs) + else: + _check_channels_ordered(info, np.unique(_channel_chromophore(info))) for ch_idx in picks: ch = info['chs'][ch_idx]['ch_name'] source, detector = ch.split(' ')[0].split('_') diff --git a/mne/io/meas_info.py b/mne/io/meas_info.py index 3e8711f4489..153578dc288 100644 --- a/mne/io/meas_info.py +++ b/mne/io/meas_info.py @@ -14,13 +14,10 @@ import operator from textwrap import shorten -import re as re - import numpy as np from .pick import (channel_type, pick_channels, pick_info, - get_channel_type_constants, pick_types, - _picks_to_idx, _get_channel_types) + get_channel_type_constants, pick_types) from .constants import FIFF, _coord_frame_named from .open import fiff_open from .tree import dir_tree_find @@ -777,29 +774,6 @@ def _check_consistency(self, prepend_error=''): warn('the "filename" key is misleading ' 'and info should not have it') - def _check_nirs_ch_names(self): - picks = _picks_to_idx(self, 'fnirs', exclude=[], allow_empty=True) - ch_types = _get_channel_types(self, picks=picks) - for idx in picks: - ch_name = self['chs'][idx]['ch_name'] - if ch_name != self['ch_names'][idx]: - raise RuntimeError( - 'Bad info: info["chs"][%d]["ch_name"] not matching ' - 'info["ch_names"][%d]' % (idx, idx)) - name, suffix = ch_name.split(' ') - if suffix != ch_types[idx]: - try: - int(suffix) - except ValueError: - raise RuntimeError( - 'Bad info: info["chs"][%d]["ch_name"] has an invalid ' - 'suffix %s' % (idx, suffix)) - s, d = name.split('_') - if not re.match('^S[0-9]+$', s) or not re.match('^D[0-9]+$', d): - raise RuntimeError( - 'Bad info: info["chs"][%d]["ch_name"] has an invalid ' - 'name %s, should follow the S#_D# format' % (idx, name)) - def _update_redundant(self): """Update the redundant entries.""" self['ch_names'] = [ch['ch_name'] for ch in self['chs']] From 9228a01525a7ad2fc65efc21134e932246ec418a Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Tue, 13 Apr 2021 10:31:41 +0100 Subject: [PATCH 33/44] Simplify _set_montage_fnirs Co-authored-by: Robert Luke <748691+rob-luke@users.noreply.github.com> --- mne/channels/montage.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index eef0d73c114..6d6667e143d 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -740,12 +740,12 @@ def _set_montage_fnirs(info, montage): _check_channels_ordered) # Modify info['chs'][#]['loc'] in place num_ficiduals = len(montage.dig) - len(montage.ch_names) - picks = _picks_to_idx(info, 'fnirs', exclude=[], allow_empty=True) freqs = np.unique(_channel_frequencies(info)) if freqs.size > 0: - _check_channels_ordered(info, freqs) + picks = _check_channels_ordered(info, freqs) else: - _check_channels_ordered(info, np.unique(_channel_chromophore(info))) + picks = _check_channels_ordered(info, + np.unique(_channel_chromophore(info))) for ch_idx in picks: ch = info['chs'][ch_idx]['ch_name'] source, detector = ch.split(' ')[0].split('_') From e971fe8886cf1900c54b221ad94e4d895fe3c927 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Tue, 13 Apr 2021 11:09:48 +0100 Subject: [PATCH 34/44] Add tests for channel variations --- mne/channels/tests/test_standard_montage.py | 37 +++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index fed801adaa7..da7af9a9909 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -101,7 +101,7 @@ def _simulate_artinis_octamon(): info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=sfreq) for i, ch_name in enumerate(ch_names): info['chs'][i]['loc'][9] = int(ch_name.split(' ')[1]) - raw = RawArray(data, info, verbose=True) + raw = RawArray(data, info) return raw @@ -127,7 +127,7 @@ def _simulate_artinis_brite23(): ch_types.append('hbr') sfreq = 10. # Hz info = create_info(ch_names=ch_names, ch_types=ch_types, sfreq=sfreq) - raw = RawArray(data, info, verbose=True) + raw = RawArray(data, info) return raw @@ -202,3 +202,36 @@ def test_set_montage_artinis(): # except the 10th which is the wavelength if not hbo and hbr type) assert_array_almost_equal(raw.info['chs'][0]['loc'][:9], raw.info['chs'][1]['loc'][:9]) + + # Test channel variations + raw_old = _simulate_artinis_brite23() + # Raw missing some channels that are in the montage: pass + raw = raw_old.copy() + raw.pick(['S1_D1 hbo', 'S1_D1 hbr']) + raw.set_montage('artinis-brite23') + + # Unconventional channel pair: pass + raw = raw_old.copy() + info_new = create_info(['S11_D1 hbo', 'S11_D1 hbr'], raw.info['sfreq'], + ['hbo', 'hbr']) + new = RawArray(np.random.normal(size=(2, len(raw))), info_new) + raw.add_channels([new], force_update_info=True) + raw.set_montage('artinis-brite23') + + # Source not in montage: fail + raw = raw_old.copy() + info_new = create_info(['S12_D7 hbo', 'S12_D7 hbr'], raw.info['sfreq'], + ['hbo', 'hbr']) + new = RawArray(np.random.normal(size=(2, len(raw))), info_new) + raw.add_channels([new], force_update_info=True) + with pytest.raises(ValueError, match='is not in list'): + raw.set_montage('artinis-brite23') + + # Detector not in montage: fail + raw = raw_old.copy() + info_new = create_info(['S11_D8 hbo', 'S11_D8 hbr'], raw.info['sfreq'], + ['hbo', 'hbr']) + new = RawArray(np.random.normal(size=(2, len(raw))), info_new) + raw.add_channels([new], force_update_info=True) + with pytest.raises(ValueError, match='is not in list'): + raw.set_montage('artinis-brite23') From 5fac262883beb6daecb34f0b4e9632dc08b79b9e Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 14 Apr 2021 08:29:27 -0400 Subject: [PATCH 35/44] TST: Test rough equivalent with fsaverage --- mne/channels/tests/test_standard_montage.py | 43 ++++++++++++--------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index da7af9a9909..4c27c3443cb 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -7,19 +7,18 @@ import pytest import numpy as np -import os.path as op from numpy.testing import (assert_allclose, assert_array_almost_equal, assert_raises) -from mne import create_info, read_trans -from mne.channels import make_standard_montage +from mne import create_info +from mne.channels import make_standard_montage, compute_native_head_t +from mne.channels.montage import get_builtin_montages, HEAD_SIZE_DEFAULT from mne.io import RawArray from mne.io._digitization import _get_dig_eeg, _get_fid_coords -from mne.channels.montage import get_builtin_montages, HEAD_SIZE_DEFAULT -from mne.channels import compute_native_head_t from mne.io.constants import FIFF from mne.preprocessing.nirs import optical_density, beer_lambert_law +from mne.transforms import _get_trans, _angle_between_quats, rot_to_quat @pytest.mark.parametrize('kind', get_builtin_montages()) @@ -132,22 +131,30 @@ def _simulate_artinis_brite23(): return raw -def test_set_montage_artinis(): - """Test that OctaMon and Brite 23 montages are set properly.""" +@pytest.mark.parametrize('kind', ('octamon', 'brite23')) +def test_set_montage_artinis_fsaverage(kind): + """Test that artinis montages match fsaverage's head<->MRI transform.""" # Compare OctaMon and Brite23 to fsaverage + trans_fs, _ = _get_trans('fsaverage') + montage = make_standard_montage(f'artinis-{kind}') + trans = compute_native_head_t(montage) + assert trans['to'] == trans_fs['to'] + assert trans['from'] == trans_fs['from'] + translation = 1000 * np.linalg.norm(trans['trans'][:3, 3] - + trans_fs['trans'][:3, 3]) + # TODO: This is actually quite big... + assert 15 < translation < 18 # mm + rotation = np.rad2deg( + _angle_between_quats(rot_to_quat(trans['trans'][:3, :3]), + rot_to_quat(trans_fs['trans'][:3, :3]))) + assert 3 < rotation < 7 # degrees + + +def test_set_montage_artinis_basic(): + """Test that OctaMon and Brite 23 montages are set properly.""" + # Test OctaMon montage montage_octamon = make_standard_montage('artinis-octamon') - trans_octamon = compute_native_head_t(montage_octamon) montage_brite23 = make_standard_montage('artinis-brite23') - trans_brite = compute_native_head_t(montage_brite23) - fif = op.join(op.dirname(__file__), '..', '..', 'data', 'fsaverage', - 'fsaverage-trans.fif') - fsaverage = read_trans(fif) - assert_array_almost_equal(list(trans_octamon.values())[2], - list(fsaverage.values())[2]) - assert_array_almost_equal(list(trans_brite.values())[2], - list(fsaverage.values())[2]) - - # Test OctaMon montage raw = _simulate_artinis_octamon() raw_od = optical_density(raw) old_info = raw.info.copy() From 2640d1210827814d4b33670df96f35d16aee7fea Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Fri, 16 Apr 2021 11:17:53 +0100 Subject: [PATCH 36/44] Test 2 more built in montages (OctaMon & Brite 23) --- mne/channels/tests/test_montage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/channels/tests/test_montage.py b/mne/channels/tests/test_montage.py index 34db8132fd5..208fdc2b323 100644 --- a/mne/channels/tests/test_montage.py +++ b/mne/channels/tests/test_montage.py @@ -1498,7 +1498,7 @@ def test_read_dig_hpts(): def test_get_builtin_montages(): """Test help function to obtain builtin montages.""" - EXPECTED_NUM = 24 + EXPECTED_NUM = 26 assert len(get_builtin_montages()) == EXPECTED_NUM From 2b17fb01717df11839ffcb5bc063e95ffdaf11aa Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Fri, 16 Apr 2021 14:58:49 +0100 Subject: [PATCH 37/44] Update tutorial --- tutorials/io/plot_30_reading_fnirs_data.py | 99 +++++++++------------- 1 file changed, 42 insertions(+), 57 deletions(-) diff --git a/tutorials/io/plot_30_reading_fnirs_data.py b/tutorials/io/plot_30_reading_fnirs_data.py index 85002a80d9f..fce45809b07 100644 --- a/tutorials/io/plot_30_reading_fnirs_data.py +++ b/tutorials/io/plot_30_reading_fnirs_data.py @@ -65,28 +65,20 @@ Loading legacy data in csv or tsv format ======================================== -.. warning:: This method is not supported. You should convert your data - to the `SNIRF `_ - format using the tools provided by the Society - for functional Near-Infrared Spectroscopy, and then load it - using :func:`mne.io.read_raw_snirf`. - - This method will only work for data that has already been - converted to oxyhaemoglobin and deoxyhaemoglobin. It will not work - with raw intensity data or optical density data. - -Many legacy fNIRS measurements are stored in csv and tsv formats. -These formats are not officially supported in MNE as there is no -standardisation of the file format - -the naming and ordering of channels, the type and scaling of data, and -specification of sensor positions varies between each vendor. -Instead, we suggest that data is converted to the format approved by the -Society for functional Near-Infrared Spectroscopy called -`SNIRF `_, -they provide a number of tools to convert your legacy -data to the SNIRF format. -However, due to the prevalence of these legacy files we provide -a template example of how you may read data in t/csv formats. +.. warning:: This method is not supported and users are discoraged to use it. + You should convert your data to the + `SNIRF `_ format using the tools + provided by the Society for functional Near-Infrared Spectroscopy, + and then load it using :func:`mne.io.read_raw_snirf`. + +fNIRS measurements often have a non-standardised format that is not supported +by MNE and cannot be converted easily into SNIRF. However if it is possible to +store this data in a legacy csv or tsv format, we show here a way to load it +even though it is not officially supported by MNE due to the lack of +standardisation of the file format (the naming and ordering of channels, the +type and scaling of data, and specification of sensor positions varies between +each vendor). You will likely have to adapt this depending on the system from +which your CSV originated. """ # noqa:E501 import numpy as np @@ -106,11 +98,9 @@ ############################################################################### # -# .. warning:: You must ensure that the channel naming structure follows -# the MNE format of S#_D# type. -# The channels must be ordered in pairs haemoglobin pairs, -# such that for a single channel all the types are in subsequent -# indices. The type order must be hbo then hbr. +# .. warning:: The channels must be ordered in haemoglobin pairs, such that for +# a single channel all the types are in subsequent indices. The +# type order must be 'hbo' then 'hbr'. # The data below is already in the correct order and may be # used as a template for how data must be stored. # If the order that your data is stored is different to the @@ -124,10 +114,9 @@ data = pd.read_csv('fnirs.csv') -# In MNE the naming of channels MUST follow this structure of -# `S#_D# type` where # is replaced -# by the appropriate source and detector number and type is -# either hbo or hbr. +# In MNE the naming of channels MUST follow this structure of `S#_D# type` +# where # is replaced by the appropriate source and detector numbers and type +# is either hbo or hbr. ch_names = ['S1_D1 hbo', 'S1_D1 hbr', 'S2_D1 hbo', 'S2_D1 hbr', 'S3_D1 hbo', 'S3_D1 hbr', 'S4_D1 hbo', 'S4_D1 hbr', @@ -137,16 +126,16 @@ 'hbo', 'hbr', 'hbo', 'hbr', 'hbo', 'hbr', 'hbo', 'hbr', 'hbo', 'hbr', 'hbo', 'hbr'] -sfreq = 10. # Hz +sfreq = 10. # in Hz ############################################################################### # Finally, the data can be converted in to an MNE data structure. # The metadata above is used to create an :class:`mne.Info` data structure, -# and this is combined with the data to create -# an MNE :class:`~mne.io.Raw` object. For more details on the info structure -# see :ref:`tut-info-class`, and for additional details on how continuous -# data is stored in MNE see :ref:`tut-raw-class`. +# and this is combined with the data to create an MNE :class:`~mne.io.Raw` +# object. For more details on the info structure see :ref:`tut-info-class`, and +# for additional details on how continuous data is stored in MNE see +# :ref:`tut-raw-class`. # For a more extensive description of how to create MNE data structures from # raw array data see :ref:`tut_creating_data_structures`. @@ -159,11 +148,10 @@ # --------------------------------------------------- # # Having information about optode locations may assist in your analysis. -# Beyond the general benefits this provides -# (e.g. creating regions of interest, etc), -# this is particularly important for fNIRS as information about the -# distance between optodes is required to convert the optical density data -# in to an estimate of the haemoglobin concentrations. +# Beyond the general benefits this provides (e.g. creating regions of interest, +# etc), this is may be particularly important for fNIRS as information about +# the optode locations is required to convert the optical density data in to an +# estimate of the haemoglobin concentrations. # MNE provides methods to load standard sensor configurations (montages) from # some vendors, and this is demonstrated below. # Some handy tutorials for understanding sensor locations, coordinate systems, @@ -172,36 +160,33 @@ # :ref:`ex-eeg-scalp`. # # Below is an example of how to load the optode positions for an Artinis -# OctaMon device. However, many fNIRS researchers use custom optode montages, -# in this case you can generate your own .elc file (see `example file -# `_) and load that instead. +# OctaMon device. -raw.set_montage('artinis-octamon') +montage = mne.channels.make_standard_montage('artinis-octamon') +raw.set_montage(montage) # View the position of optodes in 2D to confirm the positions are correct. raw.plot_sensors() ############################################################################### -# To validate the positions were loaded correctly it is also possible -# to view the location of the sources (red), detectors (black), -# and channel (white lines and orange dots) locations in a 3D representation. +# To validate the positions were loaded correctly it is also possible to view +# the location of the sources (red), detectors (black), and channels (white +# lines and orange dots) in a 3D representation. # The ficiduals are marked in blue, green and red. # See :ref:`plot_source_alignment` for more details. subjects_dir = mne.datasets.sample.data_path() + '/subjects' mne.datasets.fetch_fsaverage(subjects_dir=subjects_dir) +trans = mne.channels.compute_native_head_t(montage) + fig = mne.viz.create_3d_figure(size=(800, 600), bgcolor='white') -fig = mne.viz.plot_alignment(raw.info, show_axes=True, - subject='fsaverage', coord_frame='mri', - trans='fsaverage', surfaces=['brain', 'head'], - fnirs=['channels', 'pairs', - 'sources', 'detectors'], - dig=True, mri_fiducials=True, - subjects_dir=subjects_dir, fig=fig) -mne.viz.set_3d_view(figure=fig, azimuth=90, elevation=90, distance=0.4, +fig = mne.viz.plot_alignment( + raw.info, trans=trans, subject='fsaverage', subjects_dir=subjects_dir, + surfaces=['brain', 'head'], coord_frame='mri', dig=True, show_axes=True, + fnirs=['channels', 'pairs', 'sources', 'detectors'], fig=fig) +mne.viz.set_3d_view(figure=fig, azimuth=90, elevation=90, distance=0.5, focalpoint=(0., -0.01, 0.02)) From 227fc24f2272fc48cbe8ac8864061f7cc4155f3d Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Fri, 16 Apr 2021 15:10:22 +0100 Subject: [PATCH 38/44] Describe changes --- doc/changes/latest.inc | 2 ++ mne/channels/montage.py | 2 +- mne/channels/tests/test_standard_montage.py | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index 076fb4d9d0c..b9db1069455 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -181,6 +181,8 @@ Enhancements - :func:`mne.preprocessing.find_eog_events` and :func:`mne.preprocessing.create_eog_epochs` now accept a list of channel names, allowing you to specify multiple EOG channels at once (:gh:`9269` by `Richard Höchenberger`_) +- Add support for setting montages on fNIRS data, with built in standard montages for Artinis OctaMon and Artinis Brite23 devices (:gh:`9141` by `Johann Benerradi`_ and `Robert Luke`_) + Bugs ~~~~ - Fix bug with :func:`mne.viz.plot_evoked_topo` where set ylim parameters gets swapped across channel types. (:gh:`9207` by |Ram Pari|_) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index 6d6667e143d..8936f7e052a 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -1415,7 +1415,7 @@ def make_standard_montage(kind, head_size=HEAD_SIZE_DEFAULT): artinis-octamon Artinis OctaMon fNIRS (8 sources, 2 detectors) - artinis-brite23 Artinis Brite 23 fNIRS (11 sources, 7 detectors) + artinis-brite23 Artinis Brite23 fNIRS (11 sources, 7 detectors) =================== ===================================================== .. versionadded:: 0.19.0 diff --git a/mne/channels/tests/test_standard_montage.py b/mne/channels/tests/test_standard_montage.py index 4c27c3443cb..f8928584c20 100644 --- a/mne/channels/tests/test_standard_montage.py +++ b/mne/channels/tests/test_standard_montage.py @@ -106,7 +106,7 @@ def _simulate_artinis_octamon(): def _simulate_artinis_brite23(): - """Simulate artinis Brite 23 channel data from numpy data. + """Simulate artinis Brite23 channel data from numpy data. This is to test data that is imported with missing or incorrect montage info. This data can then be used to test the set_montage function. @@ -151,7 +151,7 @@ def test_set_montage_artinis_fsaverage(kind): def test_set_montage_artinis_basic(): - """Test that OctaMon and Brite 23 montages are set properly.""" + """Test that OctaMon and Brite23 montages are set properly.""" # Test OctaMon montage montage_octamon = make_standard_montage('artinis-octamon') montage_brite23 = make_standard_montage('artinis-brite23') @@ -190,7 +190,7 @@ def test_set_montage_artinis_basic(): assert_array_almost_equal(raw_hb.info['chs'][0]['loc'][:9], raw_hb.info['chs'][1]['loc'][:9]) - # Test Brite 23 montage + # Test Brite23 montage raw = _simulate_artinis_brite23() old_info = raw.info.copy() raw.set_montage(montage_brite23) From 717f2e692b15f63fb9cbf70d3bfed14fd804ebf5 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Fri, 16 Apr 2021 15:28:05 +0100 Subject: [PATCH 39/44] Update latest --- 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 b9db1069455..d6c508e11f1 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -181,7 +181,7 @@ Enhancements - :func:`mne.preprocessing.find_eog_events` and :func:`mne.preprocessing.create_eog_epochs` now accept a list of channel names, allowing you to specify multiple EOG channels at once (:gh:`9269` by `Richard Höchenberger`_) -- Add support for setting montages on fNIRS data, with built in standard montages for Artinis OctaMon and Artinis Brite23 devices (:gh:`9141` by `Johann Benerradi`_ and `Robert Luke`_) +- Add support for setting montages on fNIRS data, with built in standard montages for Artinis OctaMon and Artinis Brite23 devices (:gh:`9141` by `Johann Benerradi`_, `Robert Luke`_ and `Eric Larson`_) Bugs ~~~~ From 46908304b6243ee0aff5c0a5561059b1a5510ad3 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Thu, 22 Apr 2021 09:10:56 +0100 Subject: [PATCH 40/44] Apply suggestions from code review Co-authored-by: Robert Luke <748691+rob-luke@users.noreply.github.com> --- mne/channels/montage.py | 6 ++-- tutorials/io/plot_30_reading_fnirs_data.py | 35 ++++++++++++---------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index 8936f7e052a..71a8c3397ec 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -738,14 +738,16 @@ def _set_montage_fnirs(info, montage): from ..preprocessing.nirs import (_channel_frequencies, _channel_chromophore, _check_channels_ordered) - # Modify info['chs'][#]['loc'] in place - num_ficiduals = len(montage.dig) - len(montage.ch_names) + # Validate that the fNIRS info is correctly formatted freqs = np.unique(_channel_frequencies(info)) if freqs.size > 0: picks = _check_channels_ordered(info, freqs) else: picks = _check_channels_ordered(info, np.unique(_channel_chromophore(info))) + + # Modify info['chs'][#]['loc'] in place + num_ficiduals = len(montage.dig) - len(montage.ch_names) for ch_idx in picks: ch = info['chs'][ch_idx]['ch_name'] source, detector = ch.split(' ')[0].split('_') diff --git a/tutorials/io/plot_30_reading_fnirs_data.py b/tutorials/io/plot_30_reading_fnirs_data.py index fce45809b07..9f405557173 100644 --- a/tutorials/io/plot_30_reading_fnirs_data.py +++ b/tutorials/io/plot_30_reading_fnirs_data.py @@ -62,7 +62,7 @@ will also read the ``digaux`` data and create annotations for any triggers. -Loading legacy data in csv or tsv format +Loading legacy data in CSV or TSV format ======================================== .. warning:: This method is not supported and users are discoraged to use it. @@ -71,14 +71,13 @@ provided by the Society for functional Near-Infrared Spectroscopy, and then load it using :func:`mne.io.read_raw_snirf`. -fNIRS measurements often have a non-standardised format that is not supported -by MNE and cannot be converted easily into SNIRF. However if it is possible to -store this data in a legacy csv or tsv format, we show here a way to load it -even though it is not officially supported by MNE due to the lack of -standardisation of the file format (the naming and ordering of channels, the -type and scaling of data, and specification of sensor positions varies between -each vendor). You will likely have to adapt this depending on the system from -which your CSV originated. +fNIRS measurements can have a non-standardised format that is not supported by +MNE and cannot be converted easily into SNIRF. This legacy data is often in CSV +or TSV format, we show here a way to load it even though it is not officially +supported by MNE due to the lack of standardisation of the file format (the +naming and ordering of channels, the type and scaling of data, and +specification of sensor positions varies between each vendor). You will likely +have to adapt this depending on the system from which your CSV originated. """ # noqa:E501 import numpy as np @@ -88,7 +87,7 @@ # sphinx_gallery_thumbnail_number = 2 ############################################################################### -# First, we generate an example csv file which will then be loaded in to MNE. +# First, we generate an example CSV file which will then be loaded in to MNE. # This step would be skipped if you have actual data you wish to load. # We simulate 16 channels with 100 samples of data and save this to a file # called fnirs.csv. @@ -108,15 +107,19 @@ # channel naming according to the data structure, then reorder # the channels to match the required format. # -# Next, we will load the example csv file. -# The metadata must be specified manually as the csv file does not contain -# information about channel names, types, sample rate etc. +# Next, we will load the example CSV file. data = pd.read_csv('fnirs.csv') -# In MNE the naming of channels MUST follow this structure of `S#_D# type` -# where # is replaced by the appropriate source and detector numbers and type -# is either hbo or hbr. + +############################################################################### +# Then, the metadata must be specified manually as the CSV file does not +# contain information about channel names, types, sample rate etc. +# +# .. warning:: In MNE the naming of channels MUST follow the structure of +# `S#_D# type` where # is replaced by the appropriate source and +# detector numbers and type is either `hbo`, `hbr` or the +# wavelength. ch_names = ['S1_D1 hbo', 'S1_D1 hbr', 'S2_D1 hbo', 'S2_D1 hbr', 'S3_D1 hbo', 'S3_D1 hbr', 'S4_D1 hbo', 'S4_D1 hbr', From 20aa229cf747bb39f7adbbfcfae515187a5b2ab7 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Thu, 22 Apr 2021 09:23:19 +0100 Subject: [PATCH 41/44] Fix ref --- 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 9f405557173..a8d24101ef1 100644 --- a/tutorials/io/30_reading_fnirs_data.py +++ b/tutorials/io/30_reading_fnirs_data.py @@ -160,7 +160,7 @@ # Some handy tutorials for understanding sensor locations, coordinate systems, # and how to store and view this information in MNE are: # :ref:`tut-sensor-locations`, :ref:`plot_source_alignment`, and -# :ref:`ex-eeg-scalp`. +# :ref:`ex-eeg-on-scalp`. # # Below is an example of how to load the optode positions for an Artinis # OctaMon device. From de264a051674c13890311ff402e3d0fe0e1cb1a0 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Thu, 22 Apr 2021 18:47:13 +0100 Subject: [PATCH 42/44] Update in-line comments --- mne/channels/montage.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index 71a8c3397ec..4c9341a8b4c 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -905,7 +905,7 @@ def _backcompat_value(pos, ref_pos): for name, use in zip(info_names, info_names_use): _loc_view = info['chs'][info['ch_names'].index(name)]['loc'] - # XXX info['chs'][#]['loc'] modified in place + # Next line modifies info['chs'][#]['loc'] in place _loc_view[:6] = _backcompat_value(ch_pos_use[use], eeg_ref_pos) del ch_pos_use @@ -934,11 +934,11 @@ def _backcompat_value(pos, ref_pos): # in the old dig if ref_dig_point in old_dig: digpoints.append(ref_dig_point) - # XXX info['dig'] modified in place + # Next line modifies info['dig'] in place info['dig'] = _format_dig_points(digpoints, enforce_order=True) if mnt_head.dev_head_t is not None: - # XXX info['dev_head_t'] modified in place + # Next line modifies info['dev_head_t'] in place info['dev_head_t'] = Transform('meg', 'head', mnt_head.dev_head_t) # Handle fNIRS with source, detector and channel @@ -947,10 +947,10 @@ def _backcompat_value(pos, ref_pos): info = _set_montage_fnirs(info, mnt_head) else: # None case - # XXX info['dig'] modified in place + # Next line modifies info['dig'] in place info['dig'] = None for ch in info['chs']: - # XXX info['chs'][#]['loc'] modified in place + # Next line modifies info['chs'][#]['loc'] in place ch['loc'] = np.full(12, np.nan) From c695f048edd36491a41cc899ac89a0942a214634 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Thu, 22 Apr 2021 19:26:23 +0100 Subject: [PATCH 43/44] Fix refs --- tutorials/io/30_reading_fnirs_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tutorials/io/30_reading_fnirs_data.py b/tutorials/io/30_reading_fnirs_data.py index a8d24101ef1..bca216a6672 100644 --- a/tutorials/io/30_reading_fnirs_data.py +++ b/tutorials/io/30_reading_fnirs_data.py @@ -117,8 +117,8 @@ # contain information about channel names, types, sample rate etc. # # .. warning:: In MNE the naming of channels MUST follow the structure of -# `S#_D# type` where # is replaced by the appropriate source and -# detector numbers and type is either `hbo`, `hbr` or the +# ``S#_D# type`` where # is replaced by the appropriate source and +# detector numbers and type is either ``hbo``, ``hbr`` or the # wavelength. ch_names = ['S1_D1 hbo', 'S1_D1 hbr', 'S2_D1 hbo', 'S2_D1 hbr', From 0ea038aa167857c29e421fe204ab989718a82d50 Mon Sep 17 00:00:00 2001 From: Johann Benerradi Date: Fri, 23 Apr 2021 16:39:40 +0100 Subject: [PATCH 44/44] Move channel info and add read_custom_montage --- tutorials/io/30_reading_fnirs_data.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/tutorials/io/30_reading_fnirs_data.py b/tutorials/io/30_reading_fnirs_data.py index bca216a6672..4bb92aa9330 100644 --- a/tutorials/io/30_reading_fnirs_data.py +++ b/tutorials/io/30_reading_fnirs_data.py @@ -9,6 +9,13 @@ MNE includes various functions and utilities for reading NIRS data and optode locations. +fNIRS devices consist of light sources and light detectors. A channel is formed +by source-detector pairs. MNE stores the location of the channels, sources, and +detectors. + +.. warning:: Information about device light wavelength is stored in channel + names. Manual modification of channel names is not recommended. + .. _import-nirx: NIRx (directory) @@ -164,6 +171,10 @@ # # Below is an example of how to load the optode positions for an Artinis # OctaMon device. +# +# .. note:: It is also possible to create a custom montage from a file for +# fNIRS with :func:`mne.channels.read_custom_montage` by setting +# ``coord_frame`` to ``'mri'``. montage = mne.channels.make_standard_montage('artinis-octamon') raw.set_montage(montage) @@ -191,17 +202,3 @@ fnirs=['channels', 'pairs', 'sources', 'detectors'], fig=fig) mne.viz.set_3d_view(figure=fig, azimuth=90, elevation=90, distance=0.5, focalpoint=(0., -0.01, 0.02)) - - -############################################################################### -# Storing of optode locations -# =========================== -# -# fNIRS devices consist of light sources and light detectors. -# A channel is formed by source-detector pairs. -# MNE stores the location of the channels, sources, and detectors. -# -# -# .. warning:: Information about device light wavelength is stored in -# channel names. Manual modification of channel names is not -# recommended.