Skip to content

Commit

Permalink
Automatic info['nchan'] and info['ch_names']
Browse files Browse the repository at this point in the history
The info object has two redundant fields: `nchan` and `ch_names`. They are
there as convenience fields. However, whenever the `chs` list is
updated, these fields need to be manually updated as well.

This PR makes these fields behave more like properties.

It does so by making `Info` a subclass of `collections.MutableMapping`, which allows
it to redefine `__setitem__` and `__getitem__` while retaining full
compatibility with the default Python dict.

The `nchan` field just maps to `len(info['chs'])`.

The `ch_names` field is a bit more tricky. From the outside, it behaves
as a mapping to `[ch['ch_name'] for ch in info['chs']]`. However, in order
not to generate a new list every time the field is accessed, the field
is an instance of `_ChannelNameList`.

The `_ChannelNameList` class is a subclass of `collections.Sequence`, thus
implementing a list that is read-only, but otherwise fully compatible
with a normal Python list. It overwrites the `__getitem(self, index)__` method to map
to `info['chs'][index]['ch_name']` on the fly.

The rest of the code is updated to no longer set the `nchan` and
`ch_names` fields of Info objects.
  • Loading branch information
wmvanvliet committed Feb 3, 2016
1 parent 436733b commit 3ec817c
Show file tree
Hide file tree
Showing 31 changed files with 389 additions and 126 deletions.
2 changes: 2 additions & 0 deletions doc/whats_new.rst
Expand Up @@ -47,6 +47,8 @@ API

- Deprecated function :func:`mne.time_frequency.multitaper_psd` and replaced by :func:`mne.time_frequency.psd_multitaper` by `Chris Holdgraf`_

- The `'ch_names'` and `'nchan'` fields of the :class:`mne.io.Info` class are now read-only and automatically update to accommodate changes in the `'chs'` field, by `Marijn van Vliet`_

.. _changes_0_11:

Version 0.11
Expand Down
1 change: 0 additions & 1 deletion mne/channels/channels.py
Expand Up @@ -651,7 +651,6 @@ def rename_channels(info, mapping):

# do the reampping in info
info['bads'] = bads
info['ch_names'] = ch_names
for ch, ch_name in zip(info['chs'], ch_names):
ch['ch_name'] = ch_name
info._check_consistency()
Expand Down
3 changes: 2 additions & 1 deletion mne/channels/layout.py
Expand Up @@ -18,6 +18,7 @@
from ..transforms import _polar_to_cartesian, _cartesian_to_sphere
from ..io.pick import pick_types
from ..io.constants import FIFF
from ..io.meas_info import Info
from ..utils import _clean_names
from ..externals.six.moves import map

Expand Down Expand Up @@ -411,7 +412,7 @@ def find_layout(info, ch_type=None, exclude='bads'):
layout_name = 'Vectorview-grad'
elif ((has_eeg_coils_only and ch_type in [None, 'eeg']) or
(has_eeg_coils_and_meg and ch_type == 'eeg')):
if not isinstance(info, dict):
if not isinstance(info, (dict, Info)):
raise RuntimeError('Cannot make EEG layout, no measurement info '
'was passed to `find_layout`')
return make_eeg_layout(info, exclude=exclude)
Expand Down
2 changes: 0 additions & 2 deletions mne/channels/montage.py
Expand Up @@ -619,7 +619,6 @@ def _set_montage(info, montage, update_ch_names=False):
"""
if isinstance(montage, Montage):
if update_ch_names:
info['ch_names'] = montage.ch_names
info['chs'] = list()
for ii, ch_name in enumerate(montage.ch_names):
ch_info = {'cal': 1., 'logno': ii + 1, 'scanno': ii + 1,
Expand All @@ -638,7 +637,6 @@ def _set_montage(info, montage, update_ch_names=False):
continue

ch_idx = info['ch_names'].index(ch_name)
info['ch_names'][ch_idx] = ch_name
info['chs'][ch_idx]['loc'] = np.r_[pos, [0.] * 9]
sensors_found.append(ch_idx)

Expand Down
4 changes: 1 addition & 3 deletions mne/channels/tests/test_layout.py
Expand Up @@ -44,7 +44,6 @@

test_info = _empty_info(1000)
test_info.update({
'ch_names': ['ICA 001', 'ICA 002', 'EOG 061'],
'chs': [{'cal': 1,
'ch_name': 'ICA 001',
'coil_type': 0,
Expand Down Expand Up @@ -81,7 +80,7 @@
'scanno': 376,
'unit': 107,
'unit_mul': 0}],
'nchan': 3})
})


def test_io_layout_lout():
Expand Down Expand Up @@ -229,7 +228,6 @@ def test_find_layout():
sample_info4 = copy.deepcopy(sample_info)
for ii, name in enumerate(sample_info4['ch_names']):
new = name.replace(' ', '')
sample_info4['ch_names'][ii] = new
sample_info4['chs'][ii]['ch_name'] = new

eegs = pick_types(sample_info, meg=False, eeg=True)
Expand Down
1 change: 0 additions & 1 deletion mne/evoked.py
Expand Up @@ -198,7 +198,6 @@ def __init__(self, fname, condition=None, baseline=None, proj=True,
'channel definitions are different')

info['chs'] = chs
info['nchan'] = nchan
logger.info(' Found channel information in evoked data. '
'nchan = %d' % nchan)
if sfreq > 0:
Expand Down
1 change: 0 additions & 1 deletion mne/forward/_field_interpolation.py
Expand Up @@ -211,7 +211,6 @@ def _as_meg_type_evoked(evoked, ch_type='grad', mode='fast'):
# change channel names to emphasize they contain interpolated data
for ch in evoked.info['chs']:
ch['ch_name'] += '_virtual'
evoked.info['ch_names'] = [ch['ch_name'] for ch in evoked.info['chs']]

return evoked

Expand Down
13 changes: 6 additions & 7 deletions mne/forward/_make_forward.py
Expand Up @@ -423,10 +423,9 @@ def _prepare_for_forward(src, mri_head_t, info, bem, mindist, n_jobs,
mindist, overwrite, n_jobs, verbose]
cmd = 'make_forward_solution(%s)' % (', '.join([str(a) for a in arg_list]))
mri_id = dict(machid=np.zeros(2, np.int32), version=0, secs=0, usecs=0)
info = Info(nchan=info['nchan'], chs=info['chs'], comps=info['comps'],
ch_names=info['ch_names'], dev_head_t=info['dev_head_t'],
mri_file=trans, mri_id=mri_id, meas_file=info_extra,
meas_id=None, working_dir=os.getcwd(),
info = Info(chs=info['chs'], comps=info['comps'],
dev_head_t=info['dev_head_t'], mri_file=trans, mri_id=mri_id,
meas_file=info_extra, meas_id=None, working_dir=os.getcwd(),
command_line=cmd, bads=info['bads'], mri_head_t=mri_head_t)
logger.info('')

Expand Down Expand Up @@ -549,13 +548,13 @@ def make_forward_solution(info, trans, src, bem, fname=None, meg=True,
if fname is not None and op.isfile(fname) and not overwrite:
raise IOError('file "%s" exists, consider using overwrite=True'
% fname)
if not isinstance(info, (dict, string_types)):
raise TypeError('info should be a dict or string')
if not isinstance(info, (Info, string_types)):
raise TypeError('info should be an instance of Info or string')
if isinstance(info, string_types):
info_extra = op.split(info)[1]
info = read_info(info, verbose=False)
else:
info_extra = 'info dict'
info_extra = 'instance of Info'

# Report the setup
logger.info('Source space : %s' % src)
Expand Down
3 changes: 0 additions & 3 deletions mne/forward/forward.py
Expand Up @@ -318,9 +318,6 @@ def _read_forward_meas_info(tree, fid):
chs.append(tag.data)
info['chs'] = chs

info['ch_names'] = [c['ch_name'] for c in chs]
info['nchan'] = len(chs)

# Get the MRI <-> head coordinate transformation
tag = find_tag(fid, parent_mri, FIFF.FIFF_COORD_TRANS)
coord_head = FIFF.FIFFV_COORD_HEAD
Expand Down
13 changes: 6 additions & 7 deletions mne/io/brainvision/brainvision.py
Expand Up @@ -328,10 +328,10 @@ def _get_vhdr_info(vhdr_fname, eog, misc, scale, montage):
fmt = _fmt_dict[fmt]

# load channel labels
info['nchan'] = cfg.getint('Common Infos', 'NumberOfChannels') + 1
ch_names = [''] * info['nchan']
cals = np.empty(info['nchan'])
ranges = np.empty(info['nchan'])
nchan = cfg.getint('Common Infos', 'NumberOfChannels') + 1
ch_names = [''] * nchan
cals = np.empty(nchan)
ranges = np.empty(nchan)
cals.fill(np.nan)
ch_dict = dict()
for chan, props in cfg.items('Channel Infos'):
Expand Down Expand Up @@ -437,13 +437,12 @@ def _get_vhdr_info(vhdr_fname, eog, misc, scale, montage):
# Creates a list of dicts of eeg channels for raw.info
logger.info('Setting channel info structure...')
info['chs'] = []
info['ch_names'] = ch_names
for idx, ch_name in enumerate(ch_names):
if ch_name in eog or idx in eog or idx - info['nchan'] in eog:
if ch_name in eog or idx in eog or idx - nchan in eog:
kind = FIFF.FIFFV_EOG_CH
coil_type = FIFF.FIFFV_COIL_NONE
unit = FIFF.FIFF_UNIT_V
elif ch_name in misc or idx in misc or idx - info['nchan'] in misc:
elif ch_name in misc or idx in misc or idx - nchan in misc:
kind = FIFF.FIFFV_MISC_CH
coil_type = FIFF.FIFFV_COIL_NONE
unit = FIFF.FIFF_UNIT_V
Expand Down
2 changes: 0 additions & 2 deletions mne/io/bti/bti.py
Expand Up @@ -1120,7 +1120,6 @@ def _get_bti_info(pdf_fname, config_fname, head_shape_fname, rotation_x,
info['buffer_size_sec'] = 1. # reasonable default for writing
date = bti_info['processes'][0]['timestamp']
info['meas_date'] = [date, 0]
info['nchan'] = len(bti_info['chs'])

# browse processing info for filter specs.
hp, lp = info['highpass'], info['lowpass']
Expand Down Expand Up @@ -1211,7 +1210,6 @@ def _get_bti_info(pdf_fname, config_fname, head_shape_fname, rotation_x,
chs.append(chan_info)

info['chs'] = chs
info['ch_names'] = neuromag_ch_names if rename_channels else bti_ch_names

if head_shape_fname:
logger.info('... Reading digitization points from %s' %
Expand Down
2 changes: 0 additions & 2 deletions mne/io/ctf/info.py
Expand Up @@ -389,13 +389,11 @@ def _compose_meas_info(res4, coils, trans, eeg):
if trans['t_ctf_head_head'] is not None:
info['ctf_head_t'] = trans['t_ctf_head_head']
info['chs'] = _convert_channel_info(res4, trans, eeg is None)
info['nchan'] = len(info['chs'])
info['comps'] = _convert_comp_data(res4)
if eeg is None:
# Pick EEG locations from chan info if not read from a separate file
eeg = _pick_eeg_pos(info)
_add_eeg_pos(eeg, trans, info)
info['ch_names'] = [ch['ch_name'] for ch in info['chs']]
logger.info(' Measurement info composed.')
info._check_consistency()
return info
2 changes: 0 additions & 2 deletions mne/io/edf/edf.py
Expand Up @@ -430,9 +430,7 @@ def _get_edf_info(fname, stim_channel, annot, annotmap, eog, misc, preload):
info = _empty_info(sfreq)
info['filename'] = fname
info['meas_date'] = calendar.timegm(date.utctimetuple())
info['nchan'] = nchan
info['chs'] = chs
info['ch_names'] = ch_names

if highpass.size == 0:
pass
Expand Down
1 change: 0 additions & 1 deletion mne/io/eeglab/eeglab.py
Expand Up @@ -63,7 +63,6 @@ def _get_info(eeg, montage, eog=()):
"""Get measurement info.
"""
info = _empty_info(sfreq=eeg.srate)
info['nchan'] = eeg.nbchan

# add the ch_names and info['chs'][idx]['loc']
path = None
Expand Down
3 changes: 1 addition & 2 deletions mne/io/egi/egi.py
Expand Up @@ -255,8 +255,7 @@ def __init__(self, input_fname, montage=None, eog=None, misc=None,
ch_names.extend(list(egi_info['event_codes']))
if self._new_trigger is not None:
ch_names.append('STI 014') # our new_trigger
info['nchan'] = nchan = len(ch_names)
info['ch_names'] = ch_names
nchan = len(ch_names)
for ii, ch_name in enumerate(ch_names):
ch_info = {
'cal': cal, 'logno': ii + 1, 'scanno': ii + 1, 'range': 1.0,
Expand Down
6 changes: 4 additions & 2 deletions mne/io/fiff/tests/test_raw_fiff.py
Expand Up @@ -1019,8 +1019,10 @@ def test_add_channels():
raw_meg = raw.pick_types(meg=True, eeg=False, copy=True)
raw_stim = raw.pick_types(meg=False, eeg=False, stim=True, copy=True)
raw_new = raw_meg.add_channels([raw_eeg, raw_stim], copy=True)
assert_true(all(ch in raw_new.ch_names
for ch in raw_stim.ch_names + raw_meg.ch_names))
assert_true(
all(ch in raw_new.ch_names
for ch in list(raw_stim.ch_names) + list(raw_meg.ch_names))
)
raw_new = raw_meg.add_channels([raw_eeg], copy=True)

assert_true(ch in raw_new.ch_names for ch in raw.ch_names)
Expand Down
11 changes: 4 additions & 7 deletions mne/io/kit/kit.py
Expand Up @@ -195,12 +195,12 @@ def _set_stimchannels(self, info, stim, stim_code):
% (np.max(stim),
self._raw_extras[0]['nchan']))
# modify info
info['nchan'] = self._raw_extras[0]['nchan'] + 1
nchan = self._raw_extras[0]['nchan'] + 1
ch_name = 'STI 014'
chan_info = {}
chan_info['cal'] = KIT.CALIB_FACTOR
chan_info['logno'] = info['nchan']
chan_info['scanno'] = info['nchan']
chan_info['logno'] = nchan
chan_info['scanno'] = nchan
chan_info['range'] = 1.0
chan_info['unit'] = FIFF.FIFF_UNIT_NONE
chan_info['unit_mul'] = 0
Expand All @@ -209,7 +209,6 @@ def _set_stimchannels(self, info, stim, stim_code):
chan_info['loc'] = np.zeros(12)
chan_info['kind'] = FIFF.FIFFV_STIM_CH
info['chs'].append(chan_info)
info['ch_names'].append(ch_name)
if self.preload:
err = "Can't change stim channel after preloading data"
raise NotImplementedError(err)
Expand Down Expand Up @@ -661,7 +660,7 @@ def get_kit_info(rawfile):
info = _empty_info(float(sqd['sfreq']))
info.update(meas_date=int(time.time()), lowpass=sqd['lowpass'],
highpass=sqd['highpass'], filename=rawfile,
nchan=sqd['nchan'], buffer_size_sec=1.)
buffer_size_sec=1.)

# Creates a list of dicts of meg channels for raw.info
logger.info('Setting channel info structure...')
Expand Down Expand Up @@ -738,8 +737,6 @@ def get_kit_info(rawfile):
chan_info['kind'] = FIFF.FIFFV_MISC_CH
info['chs'].append(chan_info)

info['ch_names'] = ch_names['MEG'] + ch_names['MISC']

return info, sqd


Expand Down

0 comments on commit 3ec817c

Please sign in to comment.