cosmo meeg senstype2layout mapping skl

function senstype2layout = cosmo_meeg_senstype2layout_mapping(varargin)
    % return mapping from MEEG sensor types to sensor layouts
    %
    % senstype2layout=cosmo_meeg_senstype2layout_mapping()
    %
    % Output:
    %   senstype2layout       struct where the fieldnames (keys) are senstypes
    %                         (acquisition systems with a possible suffix
    %                         indicating a type of channel)
    %                         If a senstype does not have an associated layout
    %                         then the corresponding value is set to [].
    %                         Otherwise each value has fields:
    %                         .pos     Nx2 x and y coordinates (for N channels)
    %                         .width   Nx1 channel widths
    %                         .height  Nx1 channel heights
    %                         .label   Nx1 cell with channel labels
    %                         .name    string with name of layout
    %
    % Examples:
    %     % (This example requires FieldTrip)
    %     cosmo_skip_test_if_no_external('fieldtrip');
    %     %
    %     senstype2layout=cosmo_meeg_senstype2layout_mapping();
    %     % get layout for neuromag306 MEG planar (gradiometers)
    %     layout=senstype2layout.neuromag306alt_planar;
    %     cosmo_disp(layout.label)
    %     %|| { 'MEG0113'
    %     %||   'MEG0112'
    %     %||   'MEG0122'
    %     %||      :
    %     %||   'MEG2643'
    %     %||   'COMNT'
    %     %||   'SCALE'   }@206x1
    %     cosmo_disp([layout.pos layout.width layout.height])
    %     %|| [ -0.408     0.253    0.0358    0.0369
    %     %||   -0.408     0.284    0.0358    0.0369
    %     %||   -0.328     0.285    0.0358    0.0369
    %     %||      :        :          :         :
    %     %||    0.373   -0.0821    0.0358    0.0369
    %     %||   -0.547      -0.5    0.0358    0.0369
    %     %||    0.547      -0.5    0.0358    0.0369 ]@206x4
    %     layout.name
    %     %|| 'neuromag306planar.lay'
    %
    %     % (This example requires FieldTrip)
    %     cosmo_skip_test_if_no_external('fieldtrip');
    %     %
    %     senstype2layout=cosmo_meeg_senstype2layout_mapping();
    %     % get layout for neuromag306 MEG combined planar
    %     % (combined gradiometers)
    %     layout=senstype2layout.neuromag306alt_planar_combined;
    %     cosmo_disp(layout.label)
    %     %|| { 'MEG0112+0113'
    %     %||   'MEG0122+0123'
    %     %||   'MEG0132+0133'
    %     %||         :
    %     %||   'MEG2642+2643'
    %     %||   'COMNT'
    %     %||   'SCALE'        }@104x1
    %     cosmo_disp([layout.pos layout.width layout.height])
    %     %|| [ -0.408     0.273    0.0717    0.0791
    %     %||   -0.328     0.306    0.0717    0.0791
    %     %||   -0.377     0.179    0.0717    0.0791
    %     %||      :         :         :         :
    %     %||    0.373    -0.104    0.0717    0.0791
    %     %||   -0.547      -0.5    0.0717    0.0791
    %     %||    0.547      -0.5    0.0717    0.0791 ]@104x4
    %     layout.name
    %     %|| 'neuromag306cmb.lay'
    %
    %     % (This example requires FieldTrip)
    %     cosmo_skip_test_if_no_external('fieldtrip');
    %     %
    %     senstype2layout=cosmo_meeg_senstype2layout_mapping();
    %     % get layout for EEG elec1020
    %     layout=senstype2layout.eeg1020;
    %     cosmo_disp(layout.label)
    %     %|| { 'Fp1'
    %     %||   'Fpz'
    %     %||   'Fp2'
    %     %||     :
    %     %||   'O2'
    %     %||   'COMNT'
    %     %||   'SCALE' }@23x1
    %     cosmo_disp([layout.pos layout.width layout.height])
    %     %|| [ -0.139     0.428     0.139     0.104
    %     %||        0      0.45     0.139     0.104
    %     %||    0.139     0.428     0.139     0.104
    %     %||      :         :         :         :
    %     %||    0.139    -0.428     0.139     0.104
    %     %||   -0.547      -0.5     0.139     0.104
    %     %||    0.547      -0.5     0.139     0.104 ]@23x4
    %     layout.name
    %     %|| 'EEG1020.lay'
    %
    % Notes:
    %   - this function requires FieldTrip, as it uses its collection of
    %     layouts
    %   - the output from this function is similar to FieldTrip's
    %     ft_prepare_layout, but positions are not scaled as in FieldTrip
    %   - this function caches previously read layouts, for optimization
    %     reasons. run "clear functions" to reset the cache.
    %   - this function uses cosmo_meeg_layout_collection and
    %     cosmo_meeg_senstype_collection to match sensor types with layouts
    %   - this function does not provide layouts for all sensor types; if you
    %     find a layout of interest is missing, please get in touch with the
    %     developers
    %
    % See also: cosmo_meeg_layout_collection, cosmo_meeg_senstype_collection
    %
    % #   For CoSMoMVPA's copyright information and license terms,   #
    % #   see the COPYING file distributed with CoSMoMVPA.           #

    % 'secret' options with thresholds to find best matching layout
    defaults = struct();
    defaults.thr_layout = .7;
    defaults.thr_senstype = .8;
    opt = cosmo_structjoin(defaults, varargin);

    senstype2layout = get_mapping(opt);

function senstype2layout = get_mapping(opt)
    % helper function to actually compute the mapping
    persistent cached_opt
    persistent cached_senstype2layout

    if ~isequal(opt, cached_opt) || isempty(cached_senstype2layout)
        sc = cosmo_meeg_senstype_collection();
        names = fieldnames(sc);
        n = numel(names);

        senstype2layout = struct();

        for k = 1:n
            name = names{k};
            lay = find_layout(name, opt);
            senstype2layout.(name) = lay;
        end

        cached_opt = opt;
        cached_senstype2layout = senstype2layout;
    end

    senstype2layout = cached_senstype2layout;

function pairs = senstypes_alt_names()
    % return pairs of sensor types with and without the 'alt' name, e.g.
    % neuromag306alt_planar and neuromag306_planar
    sc = cosmo_meeg_senstype_collection();
    names = fieldnames(sc);

    names_without_alt = strrep(names, 'alt', '');
    msk_with_alt = ~cosmo_match(names, names_without_alt);
    idxs_with_alt = find(msk_with_alt);
    ncandidates = numel(idxs_with_alt);

    pairs = cell(ncandidates, 2);
    pos = 0;
    for k = 1:ncandidates
        idx_with_alt = idxs_with_alt(k);
        name_without_alt = names_without_alt{idx_with_alt};
        idx_all = find(cosmo_match(names_without_alt, name_without_alt));
        idx_without_alt = setdiff(idx_all, idx_with_alt);
        switch numel(idx_without_alt)
            case 0
                continue
            case 1
                pos = pos + 1;
                name_with_alt = names{idx_with_alt};
                pairs(pos, :) = {name_without_alt, name_with_alt};
            otherwise
                assert(false);
        end
    end

    pairs = pairs(1:pos, :);

function alt_name = find_alt_name(name)
    alt_name = [];

    pairs = senstypes_alt_names();
    for col = 1:2
        msk = cosmo_match(pairs(:, col), name);
        if any(msk)
            assert(sum(msk) == 1);
            alt_name = pairs{msk, 3 - col};
            return
        end
    end

function lay = find_layout(name, opt)
    % find layout based on sensname
    % if no layout is found, lay is returned as []
    lay = find_layout_helper(name, opt);

    if ~isempty(lay)
        return
    end

    % not found, try to use the alternative name
    alt_name = find_alt_name(name);
    if ~isempty(alt_name)
        lay = find_layout_helper(alt_name, opt);
    end

function lay = find_layout_helper(name, opt)
    % helper function: first tries to get the layout directly, if that does
    % not work, try to infer planar channels from the combined channels
    sc = cosmo_meeg_senstype_collection();
    label = sc.(name).label;
    lay = find_layout_from_label(label, opt);

    if ~isempty(lay)
        return
    end

    lay = infer_planar_layout_from_combined(name, opt);

function planar_lay = infer_planar_layout_from_combined(planar_name, opt)
    % helper function in case no planar layout is found (e.g. ctf151)
    planar_lay = [];

    combined_name = [planar_name '_combined'];

    sc = cosmo_meeg_senstype_collection();
    if all(cosmo_isfield(sc, {planar_name, combined_name}))
        planar_label = sc.(planar_name).label;
        combined_label = sc.(combined_name).label;
        combined_lay = find_layout_from_label(combined_label, opt);
        if isempty(combined_lay)
            return
        end

        % get rid of COMNT and SCALE labels
        keep = cosmo_match(combined_lay.label, combined_label);
        combined_lay = slice_layout(combined_lay, keep);

        if ~isequal(combined_lay.label, combined_label)
            return
        end

        % ensure mapping between number of labels
        nlabels = size(combined_lay.pos, 1);
        combined2planar_idxs = reshape(repmat(1:nlabels, 2, 1), [], 1);

        planar_lay = slice_layout(combined_lay, combined2planar_idxs);

        planar_lay.label = reshape(planar_label', [], 1);
        planar_lay.name = [];
    end

function lay = slice_layout(lay, to_select)
    % helper function to select a subset of channels
    nrows = size(lay.pos, 1);
    keys = fieldnames(lay);
    for k = 1:numel(keys)
        key = keys{k};
        value = lay.(key);
        if size(value, 1) == nrows
            lay.(key) = value(to_select, :);
        end
    end

function lay = find_layout_from_label(label, opt)
    % use labels from the layout to identify it
    lay_coll = cosmo_meeg_layout_collection();
    lay_label = cellfun(@(x)x.label, lay_coll, 'UniformOutput', false);
    [in_lay, in_label] = cosmo_overlap(lay_label, {label});
    [coverage, i] = max(mean([in_lay, in_label], 2));

    % try to be smart and use decent thresholds
    if in_lay(i) < opt.thr_layout || coverage < opt.thr_senstype
        lay = [];
    else
        lay = lay_coll{i};
    end