From eafb61594361e0f3f49e4b9d80075c00afdf97f6 Mon Sep 17 00:00:00 2001 From: Christian Brodbeck Date: Thu, 3 Oct 2019 13:02:40 -0400 Subject: [PATCH 1/9] ADD: channels.read_dig_dat() --- mne/channels/__init__.py | 2 +- mne/channels/montage.py | 43 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/mne/channels/__init__.py b/mne/channels/__init__.py index 20f4894773c..3776e9b502c 100644 --- a/mne/channels/__init__.py +++ b/mne/channels/__init__.py @@ -6,7 +6,7 @@ from .layout import (Layout, make_eeg_layout, make_grid_layout, read_layout, find_layout, generate_2d_layout) from .montage import (DigMontage, - get_builtin_montages, make_dig_montage, + get_builtin_montages, make_dig_montage, read_dig_dat, read_dig_egi, read_dig_captrack, read_dig_fif, read_dig_polhemus_isotrak, read_polhemus_fastscan, compute_dev_head_t, make_standard_montage, diff --git a/mne/channels/montage.py b/mne/channels/montage.py index ac39bea279e..1726131523e 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -362,6 +362,49 @@ def transform_to_head(montage): return montage +def read_dig_dat(fname): + r"""Read electrode positions from .dat file + + Parameters + ---------- + fname : path-like + File from which to read electrode locations. + + Returns + ------- + montage : instance of DigMontage + The montage. + """ + fname = _check_fname(fname, overwrite='read', must_exist=True) + + with open(fname, 'r') as fid: + lines = fid.readlines() + + electrodes = {} + nasion = lpa = rpa = None + for i, line in enumerate(lines): + items = line.split() + if not items: + continue + elif len(items) != 5: + raise ValueError( + f"Error reading {fname}, line {i} has unexpected number of " + f"entries:\n{line.rstrip()}") + num = items[1] + if num == '67': + continue # centroid + pos = np.array([float(item) for item in items[2:]]) + if num == '78': + nasion = pos + elif num == '76': + lpa = pos + elif num == '82': + rpa = pos + else: + electrodes[items[0]] = pos + return make_dig_montage(electrodes, nasion, lpa, rpa) + + def read_dig_fif(fname): r"""Read digitized points from a .fif file. From 9cfdafed41182fec1da1b0adfc6c0db8f7a5754a Mon Sep 17 00:00:00 2001 From: Christian Brodbeck Date: Thu, 3 Oct 2019 17:12:52 -0400 Subject: [PATCH 2/9] DOC --- mne/channels/montage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index 1726131523e..56c18b5341c 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -180,6 +180,7 @@ class DigMontage(object): See Also -------- read_dig_captrack + read_dig_dat read_dig_egi read_dig_fif read_dig_hpts From b44e127fef742dc37a6f4fa52203fbb8f5fac3af Mon Sep 17 00:00:00 2001 From: Christian Brodbeck Date: Fri, 4 Oct 2019 15:53:01 -0400 Subject: [PATCH 3/9] DOC --- doc/changes/latest.inc | 2 ++ doc/python_reference.rst | 1 + mne/channels/montage.py | 37 ++++++++++++++++++++++++++---- mne/channels/tests/test_montage.py | 2 +- mne/io/cnt/cnt.py | 23 ++++++++++++------- 5 files changed, 51 insertions(+), 14 deletions(-) diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index 94d37770f18..8c84929f906 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -30,6 +30,8 @@ Changelog - Add reader for NIRx data in :func:`mne.io.read_raw_nirx` by `Robert Luke`_ +- Add reader for ``*.dat`` electrode position files :func:`mne.channels.read_dig_dat` by `Christian Brodbeck`_ + Bug ~~~ diff --git a/doc/python_reference.rst b/doc/python_reference.rst index 4c214acdcb5..2a6468f377f 100644 --- a/doc/python_reference.rst +++ b/doc/python_reference.rst @@ -312,6 +312,7 @@ Projections: make_dig_montage read_dig_polhemus_isotrak read_dig_captrack + read_dig_dat read_dig_egi read_dig_fif read_dig_hpts diff --git a/mne/channels/montage.py b/mne/channels/montage.py index 56c18b5341c..bc1f1ee1fe7 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -364,7 +364,14 @@ def transform_to_head(montage): def read_dig_dat(fname): - r"""Read electrode positions from .dat file + r"""Read electrode positions from a ``*.dat`` file. + + .. Warning:: + This function was implemented based on ``*.dat`` files available from + `Compumedics `_ and might not work as expected with novel + files. If it does not read your files correctly please contact the + mne-python developers. Parameters ---------- @@ -373,8 +380,23 @@ def read_dig_dat(fname): Returns ------- - montage : instance of DigMontage + montage : DigMontage The montage. + + See Also + -------- + read_dig_captrack + read_dig_dat + read_dig_egi + read_dig_fif + read_dig_hpts + read_dig_polhemus_isotrak + make_dig_montage + + Notes + ----- + ``*.dat`` files are plain text files and can be inspected and amended with + a plain text editor. """ fname = _check_fname(fname, overwrite='read', must_exist=True) @@ -426,6 +448,7 @@ def read_dig_fif(fname): See Also -------- DigMontage + read_dig_dat read_dig_egi read_dig_captrack read_dig_polhemus_isotrak @@ -466,6 +489,7 @@ def read_dig_hpts(fname, unit='mm'): -------- DigMontage read_dig_captrack + read_dig_dat read_dig_egi read_dig_fif read_dig_polhemus_isotrak @@ -555,6 +579,7 @@ def read_dig_egi(fname): -------- DigMontage read_dig_captrack + read_dig_dat read_dig_fif read_dig_hpts read_dig_polhemus_isotrak @@ -593,6 +618,7 @@ def read_dig_captrack(fname): See Also -------- DigMontage + read_dig_dat read_dig_egi read_dig_fif read_dig_hpts @@ -839,6 +865,7 @@ def read_dig_polhemus_isotrak(fname, ch_names=None, unit='m'): make_dig_montage read_polhemus_fastscan read_dig_captrack + read_dig_dat read_dig_egi read_dig_fif """ @@ -1092,9 +1119,9 @@ def make_standard_montage(kind, head_size=HEAD_SIZE_DEFAULT): Notes ----- Individualized (digitized) electrode positions should be read in using - :func:`read_dig_captrack`, :func:`read_dig_egi`, :func:`read_dig_fif`, - :func:`read_dig_polhemus_isotrak`, :func:`read_dig_hpts` or made with - :func:`make_dig_montage`. + :func:`read_dig_captrack`, :func:`read_dig_dat`, :func:`read_dig_egi`, + :func:`read_dig_fif`, :func:`read_dig_polhemus_isotrak`, + :func:`read_dig_hpts` or made with :func:`make_dig_montage`. Valid ``kind`` arguments are: diff --git a/mne/channels/tests/test_montage.py b/mne/channels/tests/test_montage.py index 856274f925c..9177096b050 100644 --- a/mne/channels/tests/test_montage.py +++ b/mne/channels/tests/test_montage.py @@ -17,7 +17,7 @@ from mne import __file__ as _mne_file, create_info, read_evokeds from mne.utils._testing import _dig_sort_key -from mne.channels import (get_builtin_montages, DigMontage, +from mne.channels import (get_builtin_montages, DigMontage, read_dig_dat, read_dig_egi, read_dig_captrack, read_dig_fif, make_standard_montage, read_custom_montage, compute_dev_head_t, make_dig_montage, diff --git a/mne/io/cnt/cnt.py b/mne/io/cnt/cnt.py index b033ba8ab62..fe183802f59 100644 --- a/mne/io/cnt/cnt.py +++ b/mne/io/cnt/cnt.py @@ -98,16 +98,23 @@ def read_raw_cnt(input_fname, eog=(), misc=(), ecg=(), """Read CNT data as raw object. .. Note:: - If montage is not provided, the x and y coordinates are read from the - file header. Channels that are not assigned with keywords ``eog``, - ``ecg``, ``emg`` and ``misc`` are assigned as eeg channels. All the eeg - channel locations are fit to a sphere when computing the z-coordinates - for the channels. If channels assigned as eeg channels have locations + 2d spatial coordinates (x, y) for EEG channels are read from the file + header and fit to a sphere to compute corresponding z-coordinates. + If channels assigned as EEG channels have locations far away from the head (i.e. x and y coordinates don't fit to a - sphere), all the channel locations will be distorted. If you are not + sphere), all the channel locations will be distorted + (all channels that are not assigned with keywords ``eog``, ``ecg``, + ``emg`` and ``misc`` are assigned as EEG channels). If you are not sure that the channel locations in the header are correct, it is - probably safer to use a (standard) montage. See - :func:`mne.channels.make_standard_montage` + probably safer to replace them with :meth:`mne.Raw.set_montage`. + Montages can be created/imported with: + + - Standard montages with :func:`mne.channels.make_standard_montage` + - Montages for `Compumedics systems `_ with + :func:`mne.channels.read_dig_dat` + - Other reader functions are listed under *See Also* at + :class:`mne.channels.DigMontage` Parameters ---------- From 176ef97d09c4eb1e2c3f137ffca0236041f2d7cd Mon Sep 17 00:00:00 2001 From: Christian Brodbeck Date: Fri, 4 Oct 2019 16:49:05 -0400 Subject: [PATCH 4/9] TEST --- mne/channels/tests/test_montage.py | 41 ++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/mne/channels/tests/test_montage.py b/mne/channels/tests/test_montage.py index 9177096b050..041cb80f515 100644 --- a/mne/channels/tests/test_montage.py +++ b/mne/channels/tests/test_montage.py @@ -3,6 +3,7 @@ # # License: BSD (3-clause) +from itertools import chain import os import os.path as op @@ -347,6 +348,46 @@ def test_read_locs(): ) +def test_read_dig_dat(): + rows = [ + ['Nasion', 78, 0.00, 1.00, 0.00], + ['Left', 76, -1.00, 0.00, 0.00], + ['Right', 82, 1.00, -0.00, 0.00], + ['O2', 69, -0.50, -0.90, 0.05], + ['Centroid', 67, 0.00, 0.00, 0.00], + ] + # write mock test.dat file + temp_dir = _TempDir() + fname_temp = op.join(temp_dir, 'test.dat') + with open(fname_temp, 'w') as fid: + for row in rows: + name = row[0].rjust(10) + data = '\t'.join(map(str, row[1:])) + fid.write(f"{name}\t{data}\n") + # construct expected value + idents = { + 78: FIFF.FIFFV_POINT_NASION, + 76: FIFF.FIFFV_POINT_LPA, + 82: FIFF.FIFFV_POINT_RPA, + 69: 1, + } + kinds = { + 78: FIFF.FIFFV_POINT_CARDINAL, + 76: FIFF.FIFFV_POINT_CARDINAL, + 82: FIFF.FIFFV_POINT_CARDINAL, + 69: FIFF.FIFFV_POINT_EEG, + } + target = {row[0]: {'r': row[2:], 'ident': idents[row[1]], + 'kind': kinds[row[1]], 'coord_frame': 0} + for row in rows[:-1]} + # read it + dig = read_dig_dat(fname_temp) + assert set(dig.ch_names) == {'O2'} + keys = chain(['Left', 'Nasion', 'Right'], dig.ch_names) + target = [target[k] for k in keys] + assert dig.dig == target + + def test_read_dig_montage_using_polhemus_fastscan(): """Test FastScan.""" N_EEG_CH = 10 From 72b4c39c7a76796dc0a95f2201b89bdb497d493f Mon Sep 17 00:00:00 2001 From: Christian Brodbeck Date: Fri, 4 Oct 2019 18:26:05 -0400 Subject: [PATCH 5/9] DOC, __all__ --- doc/_includes/dig_formats.rst | 2 ++ mne/channels/__init__.py | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/_includes/dig_formats.rst b/doc/_includes/dig_formats.rst index 9354b873ade..b10a47aec83 100644 --- a/doc/_includes/dig_formats.rst +++ b/doc/_includes/dig_formats.rst @@ -32,6 +32,8 @@ EGI .xml :func:`mne.channels.read_dig_egi` MNE-C .hpts :func:`mne.channels.read_dig_hpts` Brain Products .bvct :func:`mne.channels.read_dig_captrack` + +Compumedics .dat :func:`mne.channels.read_dig_dat` ================= ================ ============================================== To load Polhemus FastSCAN files you can use diff --git a/mne/channels/__init__.py b/mne/channels/__init__.py index 3776e9b502c..d82cf523a8a 100644 --- a/mne/channels/__init__.py +++ b/mne/channels/__init__.py @@ -25,10 +25,10 @@ 'make_standard_montage', # Readers - 'read_ch_connectivity', 'read_dig_captrack', 'read_dig_egi', - 'read_dig_fif', 'read_dig_montage', 'read_dig_polhemus_isotrak', - 'read_layout', 'read_montage', 'read_polhemus_fastscan', - 'read_custom_montage', 'read_dig_hpts', + 'read_ch_connectivity', 'read_dig_captrack', 'read_dig_dat', + 'read_dig_egi', 'read_dig_fif', 'read_dig_montage', + 'read_dig_polhemus_isotrak', 'read_layout', 'read_montage', + 'read_polhemus_fastscan', 'read_custom_montage', 'read_dig_hpts', # Helpers 'rename_channels', 'make_1020_channel_selections', From a9e0d442d75dcdd982ed71828da0d5f267e30205 Mon Sep 17 00:00:00 2001 From: Christian Brodbeck Date: Fri, 4 Oct 2019 18:29:26 -0400 Subject: [PATCH 6/9] DOC: intro --- doc/_includes/dig_formats.rst | 1 + tutorials/intro/plot_40_sensor_locations.py | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/doc/_includes/dig_formats.rst b/doc/_includes/dig_formats.rst index b10a47aec83..8a6074a1a8f 100644 --- a/doc/_includes/dig_formats.rst +++ b/doc/_includes/dig_formats.rst @@ -1,4 +1,5 @@ :orphan: +.. _dig-formats: Supported formats for digitized 3D locations ============================================ diff --git a/tutorials/intro/plot_40_sensor_locations.py b/tutorials/intro/plot_40_sensor_locations.py index 992ef44686a..34ba7bc8737 100644 --- a/tutorials/intro/plot_40_sensor_locations.py +++ b/tutorials/intro/plot_40_sensor_locations.py @@ -173,12 +173,8 @@ # ` — this is because the sensor positions in that dataset are # digitizations of the sensor positions on an actual subject's head. Depending # on what system was used to scan the positions one can use different -# reading functions (:func:`mne.channels.read_dig_captrack` for -# a CapTrak Brain Products system, :func:`mne.channels.read_dig_egi` -# for an EGI system, :func:`mne.channels.read_dig_polhemus_isotrak` for -# Polhemus ISOTRAK, :func:`mne.channels.read_dig_fif` to read from -# a `.fif` file or :func:`mne.channels.read_dig_hpts` to read MNE `.hpts` -# files. The read :class:`montage ` can then be added +# reading functions (see :ref:`dig-formats`). +# The read :class:`montage ` can then be added # to :class:`~mne.io.Raw` objects with the :meth:`~mne.io.Raw.set_montage` # method; in the sample data this was done prior to saving the # :class:`~mne.io.Raw` object to disk, so the sensor positions are already From b41ceb415781bf9547f58ab78b95447e059e731c Mon Sep 17 00:00:00 2001 From: Christian Brodbeck Date: Fri, 4 Oct 2019 23:35:00 -0400 Subject: [PATCH 7/9] DOC --- mne/io/cnt/cnt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/io/cnt/cnt.py b/mne/io/cnt/cnt.py index fe183802f59..131cdfc40ef 100644 --- a/mne/io/cnt/cnt.py +++ b/mne/io/cnt/cnt.py @@ -106,7 +106,7 @@ def read_raw_cnt(input_fname, eog=(), misc=(), ecg=(), (all channels that are not assigned with keywords ``eog``, ``ecg``, ``emg`` and ``misc`` are assigned as EEG channels). If you are not sure that the channel locations in the header are correct, it is - probably safer to replace them with :meth:`mne.Raw.set_montage`. + probably safer to replace them with :meth:`mne.io.Raw.set_montage`. Montages can be created/imported with: - Standard montages with :func:`mne.channels.make_standard_montage` From 6935a54257a833d1faadc856c3354fdc25ea3860 Mon Sep 17 00:00:00 2001 From: Christian Brodbeck Date: Sat, 5 Oct 2019 08:27:31 -0400 Subject: [PATCH 8/9] pep & py3.5 compatibility --- mne/channels/montage.py | 4 ++-- mne/channels/tests/test_montage.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mne/channels/montage.py b/mne/channels/montage.py index 553b3132dc5..c63e13de7f2 100644 --- a/mne/channels/montage.py +++ b/mne/channels/montage.py @@ -408,8 +408,8 @@ def read_dig_dat(fname): continue elif len(items) != 5: raise ValueError( - f"Error reading {fname}, line {i} has unexpected number of " - f"entries:\n{line.rstrip()}") + "Error reading %s, line %s has unexpected number of entries:\n" + "%s" % (fname, i, line.rstrip())) num = items[1] if num == '67': continue # centroid diff --git a/mne/channels/tests/test_montage.py b/mne/channels/tests/test_montage.py index 4de5d3c72d1..39844270933 100644 --- a/mne/channels/tests/test_montage.py +++ b/mne/channels/tests/test_montage.py @@ -348,6 +348,7 @@ def test_read_locs(): def test_read_dig_dat(): + """Test reading *.dat electrode locations.""" rows = [ ['Nasion', 78, 0.00, 1.00, 0.00], ['Left', 76, -1.00, 0.00, 0.00], From 3ba7ec188b703efb8ba43243aca71a1d2994dbf8 Mon Sep 17 00:00:00 2001 From: Christian Brodbeck Date: Sat, 5 Oct 2019 09:23:28 -0400 Subject: [PATCH 9/9] py3.5 --- 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 39844270933..bce8588cfb9 100644 --- a/mne/channels/tests/test_montage.py +++ b/mne/channels/tests/test_montage.py @@ -363,7 +363,7 @@ def test_read_dig_dat(): for row in rows: name = row[0].rjust(10) data = '\t'.join(map(str, row[1:])) - fid.write(f"{name}\t{data}\n") + fid.write("%s\t%s\n" % (name, data)) # construct expected value idents = { 78: FIFF.FIFFV_POINT_NASION,