Skip to content

Commit f9e62e0

Browse files
authored
MRG, ENH: Remove 15-char limit for FIF (#8574)
* ENH: Remove 15-char limit for FIF * FIX: Public API
1 parent 3f1c589 commit f9e62e0

File tree

20 files changed

+430
-126
lines changed

20 files changed

+430
-126
lines changed

doc/changes/latest.inc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ Enhancements
3636

3737
- Update the ``notebook`` 3d backend to use ``ipyvtk_simple`` for a better integration within ``Jupyter`` (:gh:`8503` by `Guillaume Favelier`_)
3838

39+
- Remove the 15-character limitation for channel names when writing to FIF format. If you need the old 15-character names, you can use something like ``raw.rename_channels({n: n[:13] for n in raw.ch_names}, allow_duplicates=True)``, by `Eric Larson`_ (:gh:`8346`)
40+
3941
- Add toggle-all button to :class:`mne.Report` HTML and ``width`` argument to :meth:`mne.Report.add_bem_to_section` (:gh:`8723` by `Eric Larson`_)
4042

4143
- Add infant template MRI dataset downloader :func:`mne.datasets.fetch_infant_template` (:gh:`8738` by `Eric Larson`_ and `Christian O'Reilly`_)

mne/channels/channels.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@
2525
_get_stim_channel, _check_fname)
2626
from ..io.compensator import get_current_comp
2727
from ..io.constants import FIFF
28-
from ..io.meas_info import anonymize_info, Info, MontageMixin, create_info
28+
from ..io.meas_info import (anonymize_info, Info, MontageMixin, create_info,
29+
_rename_comps)
2930
from ..io.pick import (channel_type, pick_info, pick_types, _picks_by_type,
3031
_check_excludes_includes, _contains_ch_type,
3132
channel_indices_by_type, pick_channels, _picks_to_idx,
3233
_get_channel_types, get_channel_type_constants,
3334
_pick_data_channels)
35+
from ..io.tag import _rename_list
3436
from ..io.write import DATE_NONE
3537
from ..io._digitization import _get_data_as_dict_from_dig
3638

@@ -471,13 +473,14 @@ def set_channel_types(self, mapping, verbose=None):
471473
warn(msg.format(", ".join(sorted(names)), *this_change))
472474
return self
473475

474-
@fill_doc
475-
def rename_channels(self, mapping):
476+
@verbose
477+
def rename_channels(self, mapping, allow_duplicates=False, verbose=None):
476478
"""Rename channels.
477479
478480
Parameters
479481
----------
480-
%(rename_channels_mapping)s
482+
%(rename_channels_mapping_duplicates)s
483+
%(verbose_meth)s
481484
482485
Returns
483486
-------
@@ -494,7 +497,7 @@ def rename_channels(self, mapping):
494497
from ..io import BaseRaw
495498

496499
ch_names_orig = list(self.info['ch_names'])
497-
rename_channels(self.info, mapping)
500+
rename_channels(self.info, mapping, allow_duplicates)
498501

499502
# Update self._orig_units for Raw
500503
if isinstance(self, BaseRaw) and self._orig_units is not None:
@@ -1150,17 +1153,16 @@ def interpolate_bads(self, reset_bads=True, mode='accurate',
11501153
return self
11511154

11521155

1153-
@fill_doc
1154-
def rename_channels(info, mapping):
1156+
@verbose
1157+
def rename_channels(info, mapping, allow_duplicates=False, verbose=None):
11551158
"""Rename channels.
11561159
1157-
.. warning:: The channel names must have at most 15 characters
1158-
11591160
Parameters
11601161
----------
11611162
info : dict
11621163
Measurement info to modify.
1163-
%(rename_channels_mapping)s
1164+
%(rename_channels_mapping_duplicates)s
1165+
%(verbose)s
11641166
"""
11651167
_validate_type(info, Info, 'info')
11661168
info._check_consistency()
@@ -1187,12 +1189,6 @@ def rename_channels(info, mapping):
11871189
for new_name in new_names:
11881190
_validate_type(new_name[1], 'str', 'New channel mappings')
11891191

1190-
bad_new_names = [name for _, name in new_names if len(name) > 15]
1191-
if len(bad_new_names):
1192-
raise ValueError('Channel names cannot be longer than 15 '
1193-
'characters. These channel names are not '
1194-
'valid : %s' % new_names)
1195-
11961192
# do the remapping locally
11971193
for c_ind, new_name in new_names:
11981194
for bi, bad in enumerate(bads):
@@ -1201,13 +1197,21 @@ def rename_channels(info, mapping):
12011197
ch_names[c_ind] = new_name
12021198

12031199
# check that all the channel names are unique
1204-
if len(ch_names) != len(np.unique(ch_names)):
1200+
if len(ch_names) != len(np.unique(ch_names)) and not allow_duplicates:
12051201
raise ValueError('New channel names are not unique, renaming failed')
12061202

12071203
# do the remapping in info
12081204
info['bads'] = bads
1205+
ch_names_mapping = dict()
12091206
for ch, ch_name in zip(info['chs'], ch_names):
1207+
ch_names_mapping[ch['ch_name']] = ch_name
12101208
ch['ch_name'] = ch_name
1209+
# .get b/c fwd info omits it
1210+
_rename_comps(info.get('comps', []), ch_names_mapping)
1211+
if 'projs' in info: # fwd might omit it
1212+
for proj in info['projs']:
1213+
proj['data']['col_names'][:] = \
1214+
_rename_list(proj['data']['col_names'], ch_names_mapping)
12111215
info._update_redundant()
12121216
info._check_consistency()
12131217

mne/channels/montage.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,12 +195,12 @@ def plot(self, scale_factor=20, show_names=True, kind='topomap', show=True,
195195
sphere=sphere)
196196

197197
@fill_doc
198-
def rename_channels(self, mapping):
198+
def rename_channels(self, mapping, allow_duplicates=False):
199199
"""Rename the channels.
200200
201201
Parameters
202202
----------
203-
%(rename_channels_mapping)s
203+
%(rename_channels_mapping_duplicates)s
204204
205205
Returns
206206
-------
@@ -209,7 +209,7 @@ def rename_channels(self, mapping):
209209
"""
210210
from .channels import rename_channels
211211
temp_info = create_info(list(self._get_ch_pos()), 1000., 'eeg')
212-
rename_channels(temp_info, mapping)
212+
rename_channels(temp_info, mapping, allow_duplicates)
213213
self.ch_names = temp_info['ch_names']
214214

215215
def save(self, fname):

mne/channels/tests/test_channels.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,6 @@ def test_rename_channels():
8383
# Test bad input
8484
pytest.raises(ValueError, rename_channels, info, 1.)
8585
pytest.raises(ValueError, rename_channels, info, 1.)
86-
# Test name too long (channel names must be less than 15 characters)
87-
A16 = 'A' * 16
88-
mapping = {'MEG 2641': A16}
89-
pytest.raises(ValueError, rename_channels, info, mapping)
9086

9187
# Test successful changes
9288
# Test ch_name and ch_names are changed

mne/cov.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
_DATA_CH_TYPES_SPLIT)
2525

2626
from .io.constants import FIFF
27-
from .io.meas_info import read_bad_channels, create_info
27+
from .io.meas_info import _read_bad_channels, create_info
2828
from .io.tag import find_tag
2929
from .io.tree import dir_tree_find
3030
from .io.write import (start_block, end_block, write_int, write_name_list,
@@ -2022,7 +2022,7 @@ def _read_cov(fid, node, cov_kind, limited=False, verbose=None):
20222022
projs = _read_proj(fid, this)
20232023

20242024
# Read the bad channel list
2025-
bads = read_bad_channels(fid, this)
2025+
bads = _read_bad_channels(fid, this, None)
20262026

20272027
# Put it together
20282028
assert dim == len(data)

mne/evoked.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
from .io.tag import read_tag
3333
from .io.tree import dir_tree_find
3434
from .io.pick import pick_types, _picks_to_idx, _FNIRS_CH_TYPES_SPLIT
35-
from .io.meas_info import read_meas_info, write_meas_info
35+
from .io.meas_info import (read_meas_info, write_meas_info,
36+
_read_extended_ch_info, _rename_list)
3637
from .io.proj import ProjMixin
3738
from .io.write import (start_file, start_block, end_file, end_block,
3839
write_int, write_string, write_float_matrix,
@@ -1080,7 +1081,9 @@ def _read_evoked(fname, condition=None, kind='average', allow_maxshield=False):
10801081
raise ValueError('Number of channels and number of '
10811082
'channel definitions are different')
10821083

1084+
ch_names_mapping = _read_extended_ch_info(chs, my_evoked, fid)
10831085
info['chs'] = chs
1086+
info['bads'][:] = _rename_list(info['bads'], ch_names_mapping)
10841087
logger.info(' Found channel information in evoked data. '
10851088
'nchan = %d' % nchan)
10861089
if sfreq > 0:

mne/forward/forward.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@
2525
from ..io.tag import find_tag, read_tag
2626
from ..io.matrix import (_read_named_matrix, _transpose_named_matrix,
2727
write_named_matrix)
28-
from ..io.meas_info import read_bad_channels, write_info
28+
from ..io.meas_info import (_read_bad_channels, write_info, _write_ch_infos,
29+
_read_extended_ch_info, _make_ch_names_mapping,
30+
_rename_list)
2931
from ..io.pick import (pick_channels_forward, pick_info, pick_channels,
3032
pick_types)
3133
from ..io.write import (write_int, start_block, end_block,
32-
write_coord_trans, write_ch_info, write_name_list,
34+
write_coord_trans, write_name_list,
3335
write_string, start_file, end_file, write_id)
3436
from ..io.base import BaseRaw
3537
from ..evoked import Evoked, EvokedArray
@@ -286,14 +288,14 @@ def _read_forward_meas_info(tree, fid):
286288
info['meas_id'] = tag.data if tag is not None else None
287289

288290
# Add channel information
289-
chs = list()
291+
info['chs'] = chs = list()
290292
for k in range(parent_meg['nent']):
291293
kind = parent_meg['directory'][k].kind
292294
pos = parent_meg['directory'][k].pos
293295
if kind == FIFF.FIFF_CH_INFO:
294296
tag = read_tag(fid, pos)
295297
chs.append(tag.data)
296-
info['chs'] = chs
298+
ch_names_mapping = _read_extended_ch_info(chs, parent_meg, fid)
297299
info._update_redundant()
298300

299301
# Get the MRI <-> head coordinate transformation
@@ -322,7 +324,8 @@ def _read_forward_meas_info(tree, fid):
322324
else:
323325
raise ValueError('MEG/head coordinate transformation not found')
324326

325-
info['bads'] = read_bad_channels(fid, parent_meg)
327+
info['bads'] = _read_bad_channels(
328+
fid, parent_meg, ch_names_mapping=ch_names_mapping)
326329
# clean up our bad list, old versions could have non-existent bads
327330
info['bads'] = [bad for bad in info['bads'] if bad in info['ch_names']]
328331

@@ -918,18 +921,17 @@ def write_forward_meas_info(fid, info):
918921
raise ValueError('Head<-->sensor transform not found')
919922
write_coord_trans(fid, meg_head_t)
920923

924+
ch_names_mapping = dict()
921925
if 'chs' in info:
922926
# Channel information
927+
ch_names_mapping = _make_ch_names_mapping(info['chs'])
923928
write_int(fid, FIFF.FIFF_NCHAN, len(info['chs']))
924-
for k, c in enumerate(info['chs']):
925-
# Scan numbers may have been messed up
926-
c = deepcopy(c)
927-
c['scanno'] = k + 1
928-
write_ch_info(fid, c)
929+
_write_ch_infos(fid, info['chs'], False, ch_names_mapping)
929930
if 'bads' in info and len(info['bads']) > 0:
930931
# Bad channels
932+
bads = _rename_list(info['bads'], ch_names_mapping)
931933
start_block(fid, FIFF.FIFFB_MNE_BAD_CHANNELS)
932-
write_name_list(fid, FIFF.FIFF_MNE_CH_NAME_LIST, info['bads'])
934+
write_name_list(fid, FIFF.FIFF_MNE_CH_NAME_LIST, bads)
933935
end_block(fid, FIFF.FIFFB_MNE_BAD_CHANNELS)
934936

935937
end_block(fid, FIFF.FIFFB_MNE_PARENT_MEAS_FILE)

mne/io/array/tests/test_array.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,15 @@ def test_long_names():
2828
info = create_info(['a' * 15 + 'b', 'a' * 16], 1000., verbose='error')
2929
data = np.empty((2, 1000))
3030
raw = RawArray(data, info)
31+
assert raw.ch_names == ['a' * 15 + 'b', 'a' * 16]
32+
# and a way to get the old behavior
33+
raw.rename_channels({k: k[:13] for k in raw.ch_names},
34+
allow_duplicates=True, verbose='error')
3135
assert raw.ch_names == ['a' * 13 + '-0', 'a' * 13 + '-1']
3236
info = create_info(['a' * 16] * 11, 1000., verbose='error')
3337
data = np.empty((11, 1000))
3438
raw = RawArray(data, info)
35-
assert raw.ch_names == ['a' * 12 + '-%s' % ii for ii in range(11)]
39+
assert raw.ch_names == ['a' * 16 + '-%s' % ii for ii in range(11)]
3640

3741

3842
def test_array_copy():

mne/io/constants.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
FIFF.FIFFB_HPI_COIL = 110 # Data acquired from one HPI coil
3232
FIFF.FIFFB_PROJECT = 111
3333
FIFF.FIFFB_CONTINUOUS_DATA = 112
34+
FIFF.FIFFB_CH_INFO = 113 # Extra channel information
3435
FIFF.FIFFB_VOID = 114
3536
FIFF.FIFFB_EVENTS = 115
3637
FIFF.FIFFB_INDEX = 116
@@ -156,6 +157,23 @@
156157
FIFF.FIFF_HPI_COIL_NO = 245 # Coil number listed by HPI measurement
157158
FIFF.FIFF_HPI_COILS_USED = 246 # List of coils finally used when the transformation was computed
158159
FIFF.FIFF_HPI_DIGITIZATION_ORDER = 247 # Which Isotrak digitization point corresponds to each of the coils energized
160+
161+
162+
#
163+
# Tags used for storing channel info
164+
#
165+
FIFF.FIFF_CH_SCAN_NO = 250 # Channel scan number. Corresponds to fiffChInfoRec.scanNo field
166+
FIFF.FIFF_CH_LOGICAL_NO = 251 # Channel logical number. Corresponds to fiffChInfoRec.logNo field
167+
FIFF.FIFF_CH_KIND = 252 # Channel type. Corresponds to fiffChInfoRec.kind field"
168+
FIFF.FIFF_CH_RANGE = 253 # Conversion from recorded number to (possibly virtual) voltage at the output"
169+
FIFF.FIFF_CH_CAL = 254 # Calibration coefficient from output voltage to some real units
170+
FIFF.FIFF_CH_LOC = 255 # Channel loc
171+
FIFF.FIFF_CH_UNIT = 256 # Unit of the data
172+
FIFF.FIFF_CH_UNIT_MUL = 257 # Unit multiplier exponent
173+
FIFF.FIFF_CH_DACQ_NAME = 258 # Name of the channel in the data acquisition system. Corresponds to fiffChInfoRec.name.
174+
FIFF.FIFF_CH_COIL_TYPE = 350 # Coil type in coil_def.dat
175+
FIFF.FIFF_CH_COORD_FRAME = 351 # Coordinate frame (integer)
176+
159177
#
160178
# Pointers
161179
#

mne/io/ctf_comp.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,32 @@ def read_ctf_comp(fid, node, chs, verbose=None):
7474
compdata : list
7575
The compensation data
7676
"""
77+
return _read_ctf_comp(fid, node, chs, None)
78+
79+
80+
def _read_ctf_comp(fid, node, chs, ch_names_mapping):
81+
"""Read the CTF software compensation data from the given node.
82+
83+
Parameters
84+
----------
85+
fid : file
86+
The file descriptor.
87+
node : dict
88+
The node in the FIF tree.
89+
chs : list
90+
The list of channels from info['chs'] to match with
91+
compensators that are read.
92+
ch_names_mapping : dict | None
93+
The channel renaming to use.
94+
%(verbose)s
95+
96+
Returns
97+
-------
98+
compdata : list
99+
The compensation data
100+
"""
101+
from .meas_info import _rename_comps
102+
ch_names_mapping = dict() if ch_names_mapping is None else ch_names_mapping
77103
compdata = []
78104
comps = dir_tree_find(node, FIFF.FIFFB_MNE_CTF_COMP_DATA)
79105

@@ -105,6 +131,7 @@ def read_ctf_comp(fid, node, chs, verbose=None):
105131

106132
one['save_calibrated'] = bool(calibrated)
107133
one['data'] = mat
134+
_rename_comps([one], ch_names_mapping)
108135
if not calibrated:
109136
# Calibrate...
110137
_calibrate_comp(one, chs, mat['row_names'], mat['col_names'])

0 commit comments

Comments
 (0)