Skip to content

Commit b2b5512

Browse files
lneisenmanmassich
authored andcommitted
[FIX] running numbers for duplicated channel names. (#5821)
Apply running numbers for duplicated channel names. Fix #5820
1 parent 41fc42b commit b2b5512

File tree

6 files changed

+66
-19
lines changed

6 files changed

+66
-19
lines changed

doc/whats_new.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ Changelog
4040
Bug
4141
~~~
4242

43+
- Fix :func:`mne.io.read_raw_edf` reading duplicate channel names by `lneisenman`_
44+
4345
- Fix :func:`set_bipolar_reference` in the case of generating all bipolar combinations and also in the case of repeated channels in both lists (anode and cathode) by `Cristóbal Moënne-Loccoz`_
4446

4547
- Fix missing code for computing the median when ``method='median'`` in :meth:`mne.Epochs.average` by `Cristóbal Moënne-Loccoz`_
@@ -3122,3 +3124,5 @@ of commits):
31223124
.. _Cristóbal Moënne-Loccoz: https://github.com/cmmoenne
31233125

31243126
.. _David Haslacher: https://github.com/davidhaslacher
3127+
3128+
.. _lneisenman: https://github.com/lneisenman

mne/io/edf/edf.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from ...utils import verbose, logger, warn
2020
from ..utils import _blk_read_lims
2121
from ..base import BaseRaw, _check_update_montage
22-
from ..meas_info import _empty_info, DATE_NONE
22+
from ..meas_info import _empty_info, _unique_channel_names, DATE_NONE
2323
from ..constants import FIFF
2424
from ...filter import resample
2525
from ...utils import copy_function_doc_to_method_doc
@@ -563,7 +563,6 @@ def _read_edf_header(fname, exclude):
563563
for ch in channels:
564564
fid.read(80) # transducer
565565
units = [fid.read(8).strip().decode() for ch in channels]
566-
orig_units = dict(zip(ch_names, units))
567566
edf_info['units'] = list()
568567
for i, unit in enumerate(units):
569568
if i in exclude:
@@ -572,7 +571,13 @@ def _read_edf_header(fname, exclude):
572571
edf_info['units'].append(1e-6)
573572
else:
574573
edf_info['units'].append(1)
574+
575575
ch_names = [ch_names[idx] for idx in sel]
576+
units = [units[idx] for idx in sel]
577+
578+
# make sure channel names are unique
579+
ch_names = _unique_channel_names(ch_names)
580+
orig_units = dict(zip(ch_names, units))
576581

577582
physical_min = np.array([float(fid.read(8).decode())
578583
for ch in channels])[sel]
Binary file not shown.

mne/io/edf/tests/test_edf.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
montage_path = op.join(data_dir, 'biosemi.hpts')
3535
bdf_path = op.join(data_dir, 'test.bdf')
3636
edf_path = op.join(data_dir, 'test.edf')
37+
duplicate_channel_labels_path = op.join(data_dir,
38+
'duplicate_channel_labels.edf')
3739
edf_uneven_path = op.join(data_dir, 'test_uneven_samp.edf')
3840
bdf_eeglab_path = op.join(data_dir, 'test_bdf_eeglab.mat')
3941
edf_eeglab_path = op.join(data_dir, 'test_edf_eeglab.mat')
@@ -61,7 +63,7 @@ def test_orig_units():
6163

6264
# Test original units
6365
orig_units = raw._orig_units
64-
assert len(orig_units) == 140
66+
assert len(orig_units) == len(raw.ch_names)
6567
assert orig_units['A1'] == u'µV' # formerly 'uV' edit by _check_orig_units
6668

6769

@@ -124,6 +126,15 @@ def test_edf_data():
124126
read_raw_edf(broken_fname, exclude=raw.ch_names[:132], preload=True)
125127

126128

129+
def test_duplicate_channel_labels_edf():
130+
"""Test reading edf file with duplicate channel names."""
131+
EXPECTED_CHANNEL_NAMES = ['EEG F1-Ref-0', 'EEG F2-Ref', 'EEG F1-Ref-1']
132+
with pytest.warns(RuntimeWarning, match='Channel names are not unique'):
133+
raw = read_raw_edf(duplicate_channel_labels_path, preload=False)
134+
135+
assert raw.ch_names == EXPECTED_CHANNEL_NAMES
136+
137+
127138
def test_parse_annotation(tmpdir):
128139
"""Test parsing the tal channel."""
129140
# test the parser

mne/io/meas_info.py

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,35 @@ def _stamp_to_dt(stamp):
125125
datetime.timedelta(0, 0, stamp[1])) # day, sec, μs
126126

127127

128+
def _unique_channel_names(ch_names):
129+
"""Ensure unique channel names."""
130+
FIFF_CH_NAME_MAX_LENGTH = 15
131+
unique_ids = np.unique(ch_names, return_index=True)[1]
132+
if len(unique_ids) != len(ch_names):
133+
dups = set(ch_names[x]
134+
for x in np.setdiff1d(range(len(ch_names)), unique_ids))
135+
warn('Channel names are not unique, found duplicates for: '
136+
'%s. Applying running numbers for duplicates.' % dups)
137+
for ch_stem in dups:
138+
overlaps = np.where(np.array(ch_names) == ch_stem)[0]
139+
# We need an extra character since we append '-'.
140+
# np.ceil(...) is the maximum number of appended digits.
141+
n_keep = (FIFF_CH_NAME_MAX_LENGTH - 1 -
142+
int(np.ceil(np.log10(len(overlaps)))))
143+
n_keep = min(len(ch_stem), n_keep)
144+
ch_stem = ch_stem[:n_keep]
145+
for idx, ch_idx in enumerate(overlaps):
146+
ch_name = ch_stem + '-%s' % idx
147+
if ch_name not in ch_names:
148+
ch_names[ch_idx] = ch_name
149+
else:
150+
raise ValueError('Adding a running number for a '
151+
'duplicate resulted in another '
152+
'duplicate name %s' % ch_name)
153+
154+
return ch_names
155+
156+
128157
# XXX Eventually this should be de-duplicated with the MNE-MATLAB stuff...
129158
class Info(dict):
130159
"""Measurement information.
@@ -530,22 +559,9 @@ def _check_consistency(self):
530559
self._check_ch_name_length()
531560

532561
# make sure channel names are unique
533-
unique_ids = np.unique(self['ch_names'], return_index=True)[1]
534-
if len(unique_ids) != self['nchan']:
535-
dups = set(self['ch_names'][x]
536-
for x in np.setdiff1d(range(self['nchan']), unique_ids))
537-
warn('Channel names are not unique, found duplicates for: '
538-
'%s. Applying running numbers for duplicates.' % dups)
539-
for ch_stem in dups:
540-
overlaps = np.where(np.array(self['ch_names']) == ch_stem)[0]
541-
n_keep = min(len(ch_stem),
542-
14 - int(np.ceil(np.log10(len(overlaps)))))
543-
ch_stem = ch_stem[:n_keep]
544-
for idx, ch_idx in enumerate(overlaps):
545-
ch_name = ch_stem + '-%s' % idx
546-
assert ch_name not in self['ch_names']
547-
self['ch_names'][ch_idx] = ch_name
548-
self['chs'][ch_idx]['ch_name'] = ch_name
562+
self['ch_names'] = _unique_channel_names(self['ch_names'])
563+
for idx, ch_name in enumerate(self['ch_names']):
564+
self['chs'][idx]['ch_name'] = ch_name
549565

550566
if 'filename' in self:
551567
warn('the "filename" key is misleading '

mne/io/tests/test_meas_info.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,17 @@ def test_make_info():
113113
assert info['meas_date'] is None
114114

115115

116+
def test_duplicate_name_correction():
117+
"""Test duplicate channel names with running number."""
118+
# When running number is possible
119+
info = create_info(['A', 'A', 'A'], 1000., verbose='error')
120+
assert info['ch_names'] == ['A-0', 'A-1', 'A-2']
121+
122+
# When running number is not possible
123+
with pytest.raises(ValueError, match='Adding a running number'):
124+
create_info(['A', 'A', 'A-0'], 1000., verbose='error')
125+
126+
116127
def test_fiducials_io():
117128
"""Test fiducials i/o."""
118129
tempdir = _TempDir()

0 commit comments

Comments
 (0)