Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
e0f8cc6
Add raw montage files sent by Artinis
HanBnrd Mar 17, 2021
2159fb3
Initial pseudo code
rob-luke Mar 17, 2021
44cbe02
Tweaks
rob-luke Mar 17, 2021
5dae7fa
Draft _set_montage_fnirs
HanBnrd Mar 17, 2021
e55859a
Tweaks
rob-luke Mar 17, 2021
cebdf10
Example with optode montage as elc file
HanBnrd Mar 17, 2021
9064883
Clean up and add Brite 23
HanBnrd Mar 17, 2021
4091332
Merge branch 'main' into artinis-montages
HanBnrd Mar 17, 2021
9bbf7a8
Tweak skeleton code and push example
rob-luke Mar 18, 2021
d65d8f8
Add TODOs
HanBnrd Mar 18, 2021
7428812
Update also dig
HanBnrd Mar 18, 2021
599bbfd
Fix optodes in the brain
HanBnrd Mar 18, 2021
b77459c
Add fetch_fsaverage
HanBnrd Mar 18, 2021
801c27f
Merge branch 'main' into artinis-montages
HanBnrd Mar 18, 2021
28ff382
Set montage with str in tutorial
HanBnrd Mar 19, 2021
69bc74c
Add example of how to load unstructured data [ci skip]
rob-luke Mar 20, 2021
eef41f3
Typo [skip github] [azp skip]
rob-luke Mar 20, 2021
0a35722
More typos [skip github] [azp skip]
rob-luke Mar 20, 2021
03e6944
Typos [skip github] [azp skip]
rob-luke Mar 20, 2021
8033643
Merge remote-tracking branch 'upstream/main' into artinis-montages
rob-luke Mar 20, 2021
c04ad5c
Fix linking [azp skip] [skip github]
rob-luke Mar 20, 2021
059ae2f
Small doc tweaks [azp skip] [github skip]
rob-luke Mar 20, 2021
7fc5db9
Add tests. More needed
rob-luke Mar 20, 2021
7e00eb5
More tests
rob-luke Mar 20, 2021
a47743e
Update doc and tests
HanBnrd Mar 20, 2021
4675c64
Fix pydocstyle
HanBnrd Mar 20, 2021
4fb7463
Add more warning and limitations
rob-luke Mar 20, 2021
27b8123
added wrong file
rob-luke Mar 21, 2021
d60f88e
Fix reverted source and detector
HanBnrd Mar 21, 2021
e465a1e
Merge branch 'artinis-montages' of https://github.com/HanBnrd/mne-pyt…
HanBnrd Mar 21, 2021
b7cfe66
Typo
rob-luke Mar 21, 2021
9a4db53
More warnings
rob-luke Mar 21, 2021
e4ed607
Check fNIRS ch_names and add wavelength
HanBnrd Mar 21, 2021
acc71eb
Merge branch 'main' into artinis-montages
HanBnrd Apr 7, 2021
e3a6c7b
No modification of wavelength
HanBnrd Apr 9, 2021
28e2a50
Test Brite, intensity, OD
HanBnrd Apr 11, 2021
11c30ee
Merge branch 'main' into artinis-montages
HanBnrd Apr 11, 2021
4d4a80c
Use _check_channels_ordered with new API
HanBnrd Apr 11, 2021
c990c8b
Merge branch 'main' into artinis-montages
HanBnrd Apr 12, 2021
9228a01
Simplify _set_montage_fnirs
HanBnrd Apr 13, 2021
e971fe8
Add tests for channel variations
HanBnrd Apr 13, 2021
5fac262
TST: Test rough equivalent with fsaverage
larsoner Apr 14, 2021
2640d12
Test 2 more built in montages (OctaMon & Brite 23)
HanBnrd Apr 16, 2021
2b17fb0
Update tutorial
HanBnrd Apr 16, 2021
1ffa727
Merge branch 'main' into artinis-montages
HanBnrd Apr 16, 2021
227fc24
Describe changes
HanBnrd Apr 16, 2021
717f2e6
Update latest
HanBnrd Apr 16, 2021
4690830
Apply suggestions from code review
HanBnrd Apr 22, 2021
726e282
Merge branch 'main' into artinis-montages
HanBnrd Apr 22, 2021
20aa229
Fix ref
HanBnrd Apr 22, 2021
de264a0
Update in-line comments
HanBnrd Apr 22, 2021
bade068
Merge branch 'main' into artinis-montages
HanBnrd Apr 22, 2021
c695f04
Fix refs
HanBnrd Apr 22, 2021
0ea038a
Move channel info and add read_custom_montage
HanBnrd Apr 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions mne/channels/_standard_montage_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ 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,
basename='artinis-octamon.elc'),
'artinis-brite23': partial(_mgh_or_standard,
basename='artinis-brite23.elc'),
}


Expand Down
48 changes: 48 additions & 0 deletions mne/channels/data/montages/artinis-brite23.elc
Original file line number Diff line number Diff line change
@@ -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
D1
D2
D3
D4
D5
D6
D7
S1
S2
S3
S4
S5
S6
S7
S8
S9
S10
S11
58 changes: 58 additions & 0 deletions mne/channels/data/montages/artinis-brite23.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
MNI Coordinates
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once we are sure the elc file is correct we can delete this. Must be done before merge.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For those playing along at home...

In MNE we use the terminology source (S) and detector (D). These artinis files use the terminology transmitter (Tx) and receiver (Rx).


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
32 changes: 32 additions & 0 deletions mne/channels/data/montages/artinis-octamon.elc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# ASA optode file
ReferenceLabel avg
UnitPosition mm
NumberPositions= 13
Positions
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
Nz
RPA
LPA
D1
D2
S1
S2
S3
S4
S5
S6
S7
S8
35 changes: 35 additions & 0 deletions mne/channels/data/montages/artinis-octamon.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
MNI Coordinates
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once we are sure the elc file is correct we can delete these. Must be done before merge.


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
47 changes: 45 additions & 2 deletions mne/channels/montage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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-octamon', 'artinis-brite23'
]


Expand Down Expand Up @@ -666,6 +667,35 @@ def _get_montage_in_head(montage):
return transform_to_head(montage.copy())


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.
"""
# 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:
ch = info['ch_names'][ch_idx]
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)
+ num_ficiduals]['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

# Modify info['dig'] in place
info['dig'] = montage.dig

return info


@fill_doc
def _set_montage(info, montage, match_case=True, match_alias=False,
on_missing='raise'):
Expand Down Expand Up @@ -803,6 +833,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
Expand Down Expand Up @@ -831,14 +862,22 @@ 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
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above

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)
if len(fnirs_picks) > 0:
info = _set_montage_fnirs(info, montage)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid the pain of the next person errantly using montage instead of mnt_head, after conversion above we could del montage so that it's no longer in the namespace as a strong hint that the montage-in-head should be used by the subsequent code.


else: # None case
# XXX info['dig'] modified in place
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above

info['dig'] = None
for ch in info['chs']:
# XXX info['chs'][_]['loc'] modified in place
ch['loc'] = np.full(12, np.nan)


Expand Down Expand Up @@ -1297,6 +1336,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
Expand Down
35 changes: 35 additions & 0 deletions mne/channels/tests/test_standard_montage.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,38 @@ 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 _simulate_artinis_octamon():
"""
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 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"])
return raw

def test_artinis():
raw = _simulate_artinis_octamon()
montage = make_standard_montage("artinis-octamon")
raw.set_montage(montage)
53 changes: 53 additions & 0 deletions tutorials/io/plot_30_reading_fnirs_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,56 @@
recommended.

""" # noqa:E501




Comment thread
rob-luke marked this conversation as resolved.
Outdated
###############################################################################
# Fake some data
# --------------
#
# Here we just create some fake data with the correct names for an
# artinis octamon system

import mne
from mne.channels import make_standard_montage
from mne.channels.tests.test_standard_montage import _simulate_artinis_octamon

raw_intensity = _simulate_artinis_octamon()


###############################################################################
# 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.