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 cahce.
%
% # 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
% veryify 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