function senstypes = cosmo_meeg_senstype_collection()
% return supported MEEG acquisition systems and their channel labels
%
% senstypes=cosmo_meeg_senstypes
%
% Output:
% senstypes struct where the fieldnames are the names
% of the supported MEEG acquisition systems.
% each field has fields:
% .label channel labels
% .sens short label for acquisition system
% .type one of 'meg','meg_planar','meg_axial',
% or 'eeg'. '_combined' entries have the
% 'meg' type.
% Each fieldname can have a postfix:
% '_planar_combined' combined planar
% labels; .label is Nx1 for N
% sensor locations
% '_planar' planar pair
% labels; .label is Nx2
% '_mag' magnetometers, .label
% is Nx1. Currently this is only
% provided for neuromag systems
%
% Example:
% % (This example requires FieldTrip)
% cosmo_skip_test_if_no_external('fieldtrip');
% %
% senstypes=cosmo_meeg_senstype_collection();
% %
% % show neuromag306 MEG magnetometers
% cosmo_disp(senstypes.neuromag306alt_mag)
% %|| .label
% %|| { 'MEG0111'
% %|| 'MEG0121'
% %|| 'MEG0131'
% %|| :
% %|| 'MEG2621'
% %|| 'MEG2631'
% %|| 'MEG2641' }@102x1
% %|| .sens
% %|| 'neuromag306'
% %|| .type
% %|| 'meg_axial'
% %
% % show neuromag306 MEG planar gradiometers
% cosmo_disp(senstypes.neuromag306alt_planar)
% %|| .label
% %|| { 'MEG0112' 'MEG0113'
% %|| 'MEG0122' 'MEG0123'
% %|| 'MEG0132' 'MEG0133'
% %|| : :
% %|| 'MEG2622' 'MEG2623'
% %|| 'MEG2632' 'MEG2633'
% %|| 'MEG2642' 'MEG2643' }@102x2
% %|| .sens
% %|| 'neuromag306'
% %|| .type
% %|| 'meg_planar'
% %
% % show neuromag306 MEG combined planar gradiometers
% cosmo_disp(senstypes.neuromag306alt_planar_combined)
% %|| .label
% %|| { 'MEG0112+0113'
% %|| 'MEG0122+0123'
% %|| 'MEG0132+0133'
% %|| :
% %|| 'MEG2622+2623'
% %|| 'MEG2632+2633'
% %|| 'MEG2642+2643' }@102x1
% %|| .sens
% %|| 'neuromag306'
% %|| .type
% %|| 'meg_planar_combined'
% %
% % show BTI 148 planar gradiometers
% cosmo_disp(senstypes.bti148_planar)
% %|| .label
% %|| { 'A1_dH' 'A1_dV'
% %|| 'A2_dH' 'A2_dV'
% %|| 'A3_dH' 'A3_dV'
% %|| : :
% %|| 'A146_dH' 'A146_dV'
% %|| 'A147_dH' 'A147_dV'
% %|| 'A148_dH' 'A148_dV' }@148x2
% %|| .sens
% %|| 'bti148'
% %|| .type
% %|| 'meg_planar'
%
% Note:
% - this function requires FieldTrip, as it uses its collection of
% layouts
% - this function caches previously read layouts, for optimization
% reasons. run "clear functions" to reset the cache.
%
% # For CoSMoMVPA's copyright information and license terms, #
% # see the COPYING file distributed with CoSMoMVPA. #
senstypes = get_senstypes();
function senstypes = get_senstypes()
persistent cached_senstypes
if isnumeric(cached_senstypes)
cosmo_check_external('fieldtrip');
sens_type_names = get_initial_sens_type_names();
senstypes = get_initial_senstypes(sens_type_names);
% because senslabel treats neuromag systems in a special way,
% fix the output for a more common naming scheme
% also rename 'planar_combined' to 'combined'
processors = {@fix_alt_name_senstypes, ...
@fix_neuromag306_planar_combinations, ...
@fix_neuromag306_combined_with_mag, ...
@fix_ctf275_planar_old_fieldtrip, ...
@fix_eeg10XX_channels_old_fieldtrip, ...
@fix_yokogawa440_planar_old_fieldtrip, ...
@fix_egiX_channels_old_fieldtrip, ...
@fix_neuromag122_planar_name, ...
@fix_eeg10XX_senstype, ...
@add_biosemiXXX_abc_names, ...
@add_modalities, ...
@check_siblings ...
};
nprocessors = numel(processors);
for k = 1:nprocessors
processor = processors{k};
senstypes = processor(senstypes);
end
senstypes = orderfields(senstypes);
cached_senstypes = senstypes;
else
senstypes = cached_senstypes;
end
function sens_type_names = get_initial_sens_type_names()
func_name = 'ft_senslabel';
if isempty(which(func_name))
error('Cannot find ''%s'' in search path:\n %s', ...
func_name, cosmo_strjoin(cosmo_wtf('path'), '\n '));
end
h = help(func_name);
match = regexp(cosmo_strsplit(h, '\n'), '\s*''(\w*)''\s*', 'tokens');
msk = ~cellfun(@isempty, match);
d = diff(msk);
first = find(d == 1, 1) + 1;
last = find(d == -1, 1);
sens_types_cell = match(first:last);
sens_type_names = cellfun(@(x)x{:}, sens_types_cell);
function senstypes = get_initial_senstypes(sens_type_names)
senstypes = struct();
postfixes = {'', '_combined'};
for k = 1:numel(sens_type_names)
sens_type_name = sens_type_names{k};
for j = 1:numel(postfixes)
key = [sens_type_name postfixes{j}];
label = [];
try
label = ft_senslabel(key);
catch
end
if isempty(label)
continue
end
% if the name ends with '_combined' but not with
% '_planar_combined', make it end with '_planar_combined'
% (necessary for neuromag)
sp = cosmo_strsplit(key, '_');
if numel(sp) > 1 && strcmp(sp{end}, 'combined') && ...
~strcmp(sp{end - 1}, 'planar')
sp{end} = ['planar_' sp{end}];
key = cosmo_strjoin(sp, '_');
end
sens_type_name = remove_alt_postfix(sens_type_name);
senstypes.(key).label = label;
senstypes.(key).sens = cosmo_strsplit(sens_type_name, '_', 1);
end
end
function s = remove_alt_postfix(s)
sp = cosmo_strsplit(s, 'alt');
if numel(sp) > 1 && isempty(sp{end})
s = cosmo_strjoin(sp(1:(end - 1)), 'alt');
end
function s = sort_cellstr_rows(s)
% helper function: sorts each row in a cell with strings
% used to get the planar channels in order even when old FT
% returns a weird random order
n = size(s, 1);
for k = 1:n
s(k, :) = sort(s(k, :));
end
function senstypes = fix_alt_name_senstypes(senstypes)
% fixer for neuromag, newer fieldtrip versions only
% ft_senslabel returns the label for this system in two
% varietes, with and without spaces (e.g. 'MEG 2442' and 'MEG2442')
% this function adds *alt
keys = fieldnames(senstypes);
n = numel(keys);
for k = 1:n
key = keys{k};
label = senstypes.(key).label;
% first half of the label rows are without spaces,
% second half are with spaces
% detect whether that's the case here
nrows = size(label, 1);
if mod(nrows, 2) ~= 0
continue
end
half_nrows = nrows / 2;
with_spaces = label(1:half_nrows, :);
without_spaces = label(half_nrows + (1:half_nrows), :);
% transform e.g. 'MEG 2442' to 'MEG2442'
spaces_removed = cellfun(@(x) strrep(x, ' ', ''), ...
with_spaces, 'UniformOutput', false);
if isequal(without_spaces, spaces_removed)
% insert 'alt' infix
orig_senstype = senstypes.(key);
sp = cosmo_strsplit(key, '_');
sp{1} = [sp{1} 'alt'];
alt_key = cosmo_strjoin(sp, '_');
% add new senstype for alternative name
senstypes.(alt_key) = orig_senstype;
senstypes.(alt_key).label = without_spaces;
% update label for original name
senstypes.(key) = orig_senstype;
senstypes.(key).label = with_spaces;
end
end
function senstypes = fix_neuromag122_planar_name(senstypes)
% add '_planar' suffix to neuromag122
[keys, sens] = get_keys_sens(senstypes);
idxs = find(cosmo_match(sens, 'neuromag122'));
for k = 1:numel(idxs)
key = keys{idxs(k)};
label = senstypes.(key).label;
if size(label, 2) == 2
new_key = [key '_planar'];
assert(~isfield(senstypes, new_key));
senstypes.(new_key) = senstypes.(key);
senstypes = rmfield(senstypes, key);
end
end
function senstypes = add_biosemiXXX_abc_names(senstypes)
% adds the 10/20 labels for biosemi16, 32, 64
for nch = [16 32 64]
chs = struct();
switch nch
case 16
chs.C = { '3' '4' 'z' };
chs.F = { '3' '4' 'z' };
chs.Fp = { '1' '2' };
chs.O = { '1' '2' 'z' };
chs.P = { '3' '4' 'z' };
chs.T = { '7' '8' };
case 32
chs.AF = { '3' '4' };
chs.C = { '3' '4' 'z' };
chs.CP = { '1' '2' '5' '6' };
chs.F = { '3' '4' '7' '8' 'z' };
chs.FC = { '1' '2' '5' '6' };
chs.Fp = { '1' '2' };
chs.O = { '1' '2' 'z' };
chs.P = { '3' '4' '7' '8' 'z' };
chs.PO = { '3' '4' };
chs.T = { '7' '8' };
case 64
chs.AF = { '3' '4' '7' '8' 'z' };
chs.C = { '1' '2' '3' '4' '5' '6' 'z' };
chs.CP = { '1' '2' '3' '4' '5' '6' 'z' };
chs.F = { '1' '2' '3' '4' '5' '6' '7' '8' 'z' };
chs.FC = { '1' '2' '3' '4' '5' '6' 'z' };
chs.FT = { '7' '8' };
chs.Fp = { '1' '2' 'z' };
chs.I = { 'z' };
chs.O = { '1' '2' 'z' };
chs.P = { '1' '10' '2' '3' '4' '5' '6' '7' '8' '9' 'z' };
chs.PO = { '3' '4' '7' '8' 'z' };
chs.T = { '7' '8' };
chs.TP = { '7' '8' };
end
key = sprintf('biosemi%d', nch);
if ~isfield(senstypes, key) || cosmo_overlap( ...
{senstypes.(key).label}, {{'A1', 'A2', 'A3'}}) == 1
prefixes = fieldnames(chs);
nprefix = numel(prefixes);
label_cell = cell(nprefix, 1);
for k = 1:nprefix
prefix = prefixes{k};
postfixes = chs.(prefix);
npostfix = numel(postfixes);
prefix_labels = cell(npostfix, 1);
for j = 1:npostfix
postfix = postfixes{j};
prefix_labels{j} = [prefix postfix];
end
label_cell{k} = prefix_labels;
end
labels = cat(1, label_cell{:});
alt_key = sprintf('biosemi%dalt', nch);
senstype = struct();
senstype.type = 'eeg';
senstype.sens = key;
senstype.label = labels;
senstypes.(alt_key) = senstype;
end
end
function senstypes = fix_neuromag306_planar_combinations(senstypes)
% fixer for neuromag306
% ft_senslabel does not have the name X_planar,
% but instead has the name X with label in an Nx3 cell
% with the first two columns for planar and the third for mag channels.
% This function takes the first two columns and stores them as
% a proper X_planar senstype, and takes the last column and stores them
% as an X_mag senstype.
keys = fieldnames(senstypes);
n = numel(keys);
for k = 1:n
key = keys{k};
label = senstypes.(key).label;
if size(label, 2) == 3
planar_label = label(:, 1:2);
mag_label = label(:, 3);
planar_key = [key '_planar'];
mag_key = [key '_mag'];
planar_combined_key = [planar_key '_combined'];
if ~isfield(senstypes, planar_combined_key)
senstypes = old_fieldtrip_add_combined_planar(senstypes, key);
end
assert(isfield(senstypes, planar_combined_key));
% add planar and mag keys
senstypes.(planar_key) = senstypes.(key);
senstypes.(planar_key).label = sort_cellstr_rows(planar_label);
senstypes.(mag_key) = senstypes.(key);
senstypes.(mag_key).label = mag_label;
% remove original key
senstypes = rmfield(senstypes, key);
end
end
function senstypes = fix_neuromag306_combined_with_mag(senstypes)
% since the following commit, ft_senstype supports
% ft_senslabel('neuromag306_combined')
% but its output is different than was expected in
% cosmo_meeg_senstype_collection. In particular the recent commit
% includes the magnetometers, whereas the meeg_senstype
% does not include those. This test is added as check for
% such regressions
% FieldTrip (https://github.com/fieldtrip/fieldtrip.git)
% commit 882dba3426db583f7f4f9ac0cdf4eb3c26aaefc1
% Author: Robert Oostenveld <r.oostenveld@gmail.com>
% Date: Wed Jun 22 12:47:47 2016 +0200
%
% ENH - improve combined planar MEG sensor handling, see
% http://bugzilla.fieldtriptoolbox.org/show_bug.cgi?id=3144
prefix = 'neuromag306';
suffix = '_planar_combined';
keys = fieldnames(senstypes);
n = numel(keys);
for k = 1:n
key = keys{k};
if isempty(regexp(key, ['^' prefix '.*' suffix '$'], 'once'))
continue
end
label = senstypes.(key).label;
if size(label, 2) == 2
is_non_combined_channel_mask = cellfun(@isempty, ...
regexp(label, '\+'));
keep_mask = [false, true];
illegal_label_mask = bsxfun(@xor, ~keep_mask, ...
is_non_combined_channel_mask);
if any(illegal_label_mask(:))
error(['unexpected channel order, cannot fix. Please '...
'contact the CoSMoMVPA developers']);
end
senstypes.(key).label = label(:, keep_mask);
end
end
function senstypes = fix_ctf275_planar_old_fieldtrip(senstypes)
% fixes missing channel in old fieldtrip versions
key = 'ctf275_planar';
if isfield(senstypes, key)
label = senstypes.(key).label;
% detect missing 'MRP31*' labels
if isequal(size(label), [274 2]) && ...
isequal(label([213 214], 1), {'MRP23_dH'; 'MRP32_dH'})
% allocate space
label{275, 1} = '';
% move one down
label(215:end, :) = label(214:(end - 1), :);
% insert
label(214, :) = {'MRP31_dH', 'MRP31_dV'};
senstypes.(key).label = label;
end
end
function senstypes = fix_yokogawa440_planar_old_fieldtrip(senstypes)
% fixes missing channel in old fieldtrip versions
key = 'yokogawa440_planar';
if isfield(senstypes, key)
label = senstypes.(key).label;
% detect missing 'MRP31*' labels
if isequal(size(label), [420 1]) && ...
isequal(label([1 end], 1), {'AG001_dH'; 'AG392_dV'})
label = reshape(label, [], 2);
senstypes.(key).label = label;
end
end
function senstypes = fix_eeg10XX_senstype(senstypes)
% set
keys = {'eeg1005', 'eeg1010', 'eeg1020'};
for j = 1:numel(keys)
key = keys{j};
if isfield(senstypes, key)
senstypes.(key).sens = 'ext1020';
end
end
function senstypes = fix_eeg10XX_channels_old_fieldtrip(senstypes)
% newer versions of fieldtrip add 8 channels to the eeg10XX series
keys = {'eeg1005', 'eeg1010', 'eeg1020'};
nchans = [335 86 21 875];
last_chan = {'OI2', 'I2', 'O2'};
for j = 1:numel(keys)
key = keys{j};
if isfield(senstypes, key)
label = senstypes.(key).label;
nchan = nchans(j);
if isequal(label(end), last_chan(j)) && numel(label) == nchan
to_add = {'A1' 'A2' 'M1' 'M2' 'T3' 'T4' 'T5' 'T6'}';
senstypes.(key).label = [label; to_add];
end
end
end
function senstypes = fix_egiX_channels_old_fieldtrip(senstypes)
% newer versions of fieldtrip add 8 channels to the egiX series
nchans = 2.^(5:8) + 1;
for j = 1:numel(nchans)
nchan = nchans(j);
key = sprintf('egi%d', nchan - 1);
if isfield(senstypes, key)
label = senstypes.(key).label;
wrong_last_label = sprintf('E%d', nchan);
% append label to the end
if numel(label) == nchan && strcmp(label{end}, wrong_last_label)
correct_last_label = 'Cz';
label{end + 1} = correct_last_label;
senstypes.(key).label = label;
end
end
end
function senstypes = check_siblings(senstypes)
% helper to ensure all sensor types are kosher.
% checks that "siblings" (different elements in senstypes with the same
% 'sens' value) have the same number of sensor locations
skip_test_for = {'yokogawa440', ... % has different number of
... % channels across its siblings
'ext1020'}; % for eeg1005, eeg1010, eeg1020
[keys, labels] = get_keys_sens(senstypes);
[idxs, unq_labels] = cosmo_index_unique({labels});
nunq = numel(idxs);
for k = 1:nunq
if any(cosmo_match(skip_test_for, unq_labels{1}(k)))
continue
end
idx = idxs{k};
first_key = keys{idx(1)};
first_size = size(senstypes.(first_key).label);
for j = 1:numel(idx)
key = keys{idx(j)};
label_size = size(senstypes.(key).label);
% planar systems have two columns for channel labels,
% all others have one
if endswith(key, '_planar')
ncol = 2;
else
ncol = 1;
end
if label_size(2) ~= ncol
disp(senstypes.(key));
error('%s must have %d columns in .label, found %d', ...
key, ncol, label_size(2));
end
% verify that number of channel positions matches across
% all siblings
if ~isequal(first_size(1), label_size(1))
error(['size mismatch between %s and %s: number of '...
'channel positions mismatches (%d ~= %d)'], ...
first_key, key, first_size(1), label_size(1));
end
end
end
function [keys, sens] = get_keys_sens(senstypes)
keys = fieldnames(senstypes);
sens = cellfun(@(label)senstypes.(label).sens, keys, ...
'UniformOutput', false);
function tf = endswith(s, pf)
tf = isempty(cosmo_strsplit(s, pf, -1));
function senstypes = add_modalities(senstypes)
keys = get_keys_sens(senstypes);
n = numel(keys);
candidates = {'meg_planar', 'meg_axial', 'meg', 'eeg'};
ncandidates = numel(candidates);
for k = 1:n
key = keys{k};
type = [];
if endswith(key, '_mag')
type = 'meg_axial';
elseif endswith(key, '_planar')
type = 'meg_planar';
elseif endswith(key, '_planar_combined')
type = 'meg_planar_combined';
else
label = senstypes.(key).label;
for j = 1:ncandidates
candidate = candidates{j};
if ft_senstype(label, candidate)
type = candidate;
break
end
end
end
if isempty(type)
warning('Could not find modality for %s', key);
senstypes = rmfield(senstypes, key);
continue
end
senstypes.(key).type = type;
end
function senstypes = old_fieldtrip_add_combined_planar(senstypes, key)
% helper function to deal with old fieldtrip functions.
% it's ugly because it resorts to cd-ing into FT's private directory to
% run planarchannelset
p = pwd();
cleaner = onCleanup(@()cd(p));
ft_dir = fileparts(which('ft_defaults'));
ft_priv_dir = fullfile(ft_dir, 'private');
% prepare data
label = senstypes.(key).label;
label_planar = label(:, 1:2);
hdr = struct();
hdr.Fs = [];
hdr.label = label_planar(:);
try
% go into FT's private directory
cd(ft_priv_dir);
% try to get the planar channel set
% (using private function; ugly)
planar_channel_set = planarchannelset(hdr);
planar_combined_key = [key '_planar_combined'];
senstypes.(planar_combined_key) = senstypes.(key);
senstypes.(planar_combined_key).label = planar_channel_set(:, 3);
catch
caught_error = lasterror();
if cosmo_wtf('is_matlab')
ft_me = MException(caught_error.identifier, caught_error.message);
base_me = MException('CoSMoMVPA:planarchannelset', ...
'unable to get planar channel set with old Fieldtrip');
both_me = addCause(base_me, ft_me);
throw(both_me);
else
rethrow(caught_error);
end
end