cosmo map2surface skl

function s = cosmo_map2surface(ds, fn, varargin)
    % maps a dataset structure to AFNI/SUMA NIML dset or BV SMP file
    %
    % s=cosmo_map2surface(ds, output, ...)
    %
    % Inputs:
    %   ds                  dataset struct with surface-based data
    %   output              String, indicating either the output format or the
    %                       filename.
    %                       - If output starts with a '-', then it must be one
    %                         of:
    %                           '-niml_dset'    AFNI NIML
    %                           '-bv_smp'       BrainVoyager surface map
    %                           '-gii'          GIFTI
    %                       - otherwise it must be a string indicating a file
    %                         name, and end with one of
    %                           '.niml.dset'    AFNI NIML
    %                           '.smp'          BrainVoyager surface map
    %                           '.gii'          GIFTI
    %  'encoding', e        Optional encoding for AFNI NIML or GIFTI. Depending
    %                       on the output format, supported values for e are:
    %                       - NIML:  'ascii', 'binary',
    %                                'binary.lsbfirst', 'binary.msbfirst'
    %                         (the 'binary' option uses the machine's native
    %                         format with either the least or most significant
    %                         byte first)
    %                       - GIFTI: 'ASCII', 'Base64Binary',
    %                                'GZipBase64Binary'
    %                       The encoding argument is ignored for BrainVoyager
    %                       output.
    %  'format', f          Optional format to be used for output, if the
    %                       output argument empty; one of 'niml_dset', 'bv_smp'
    %                       or 'gii'.
    %
    % Output:
    %   s                   Structure containing the surface based data based
    %                       on the output format; either a struct with
    %                       NIML data, an xff object, or a GIFTI object.
    %
    % Examples:
    %     % (this example requires the AFNI Matlab toolbox)
    %     cosmo_skip_test_if_no_external('afni');
    %     %
    %     ds=cosmo_synthetic_dataset('type','surface');
    %     %
    %     % convert to AFNIML NIML format
    %     % (to store a file to disc, use a filename as the second argument)
    %     niml=cosmo_map2surface(ds,'-niml_dset');
    %     cosmo_disp(niml);
    %     %|| .node_indices
    %     %||   [ 0         1         2         3         4         5 ]
    %     %|| .data
    %     %||   [   2.03     0.584     -1.44    -0.518      1.19     -1.33
    %     %||     -0.892      1.84    -0.262      2.34    -0.204      2.72
    %     %||     -0.826      1.17     -1.92     0.441    -0.209     0.148
    %     %||       1.16    -0.848      3.09      1.86      1.76     0.502
    %     %||       1.16      3.49     -1.37     0.479    -0.955      3.41
    %     %||      -1.29    -0.199      1.73    0.0832     0.501     -0.48 ]
    %
    % Notes:
    %   - this function is intended for datasets with surface data, i.e. with
    %     one or more values associated with each surface node. It does not
    %     support anatomical surface meshes that contain node coordinates and
    %     faces. To read and write such anatomical meshes, consider the surfing
    %     toolbox, github.com/nno/surfing
    %   - To load surface datasets, use cosmo_surface_dataset
    %
    % Dependencies:
    %   - for Brainvoyager files (.smp), it requires the NeuroElf
    %     toolbox, available from: http://neuroelf.net
    %   - for AFNI/SUMA NIML files (.niml.dset) it requires the AFNI
    %     Matlab toolbox, available from: https://github.com/afni/AFNI
    %
    % See also: cosmo_surface_dataset
    %
    % #   For CoSMoMVPA's copyright information and license terms,   #
    % #   see the COPYING file distributed with CoSMoMVPA.           #

    if ~ischar(fn)
        error('second argument must be a string');
    end

    defaults = struct();
    opt = cosmo_structjoin(defaults, varargin);

    cosmo_check_dataset(ds, 'surface');

    formats = get_formats();

    sp = cosmo_strsplit(fn, '-');
    save_to_file = ~isempty(sp{1});

    if isfield(opt, 'format')
        fmt = opt.format;
    elseif save_to_file
        fmt = get_format(formats, fn);
    else
        if numel(sp) ~= 2
            error('expected -{FORMAT}');
        end
        fmt = sp{2};
    end

    if ~isfield(formats, fmt)
        error('Unsupported format %s', fmt);
    end

    format = formats.(fmt);
    externals = format.externals;
    cosmo_check_external(externals);

    builder = format.builder;
    s = builder(ds);

    if nargout == 0
        cleaner = onCleanup(get_cleaner(format, s));
    end

    if save_to_file
        writer = format.writer;
        writer(fn, s, opt);
    end

function f = get_cleaner(format, obj)
    if isfield(format, 'cleaner')
        f = @()format.cleaner(obj);
    else
        f = @do_nothing;
    end

function do_nothing()
    % do nothing

function format = get_format(formats, fn)
    endswith = @(s, e) isempty(cosmo_strsplit(s, e, -1));

    names = fieldnames(formats);
    for k = 1:numel(names)
        name = names{k};
        exts = formats.(name).exts;
        for j = 1:numel(exts)
            if endswith(fn, exts{j})
                format = name;
                return
            end
        end
    end

    error('Unsupported extension in %s', fn);

function formats = get_formats()
    formats = struct();
    formats.niml_dset.exts = {'.niml.dset'};
    formats.niml_dset.builder = @build_niml_dset;
    formats.niml_dset.writer = @write_niml_dset;
    formats.niml_dset.externals = {'afni'};

    formats.bv_smp.exts = {'.smp'};
    formats.bv_smp.builder = @build_bv_smp;
    formats.bv_smp.writer = @write_bv_smp;
    formats.bv_smp.cleaner = @(x)x.ClearObject();
    formats.bv_smp.externals = {'neuroelf'};

    formats.gii.exts = {'.gii'};
    formats.gii.builder = @build_gifti;
    formats.gii.writer = @write_gifti;
    formats.gii.externals = {'gifti'};

    formats.pymvpa.exts = {};
    formats.pymvpa.builder = @build_pymvpa;
    formats.pymvpa.writer = @write_pymvpa_struct;
    formats.pymvpa.externals = {};

function [data, node_indices] = get_surface_data_and_node_indices(ds)
    [data, dim_labels, dim_values] = cosmo_unflatten(ds);
    if ~isequal(dim_labels, {'node_indices'})
        error('.a.fdim.labels must be {''node_indices''}');
    end

    node_indices = dim_values{1}(:)';

function p = build_pymvpa(ds)
    p = ds;

    % remove samples field
    p = rmfield(p, 'samples');

    [data, node_indices] = get_surface_data_and_node_indices(ds);
    p.samples = data;

    node_indices_base0 = int64(node_indices - 1);
    p.fa.node_indices = node_indices_base0;
    p.a = rmfield(p.a, 'fdim');

function write_pymvpa_struct(fn, m, opt)
    save(fn, '-struct', 'm');

function g = build_gifti(ds)
    s = struct();

    [data, node_indices] = get_surface_data_and_node_indices(ds);

    s.indices = node_indices(:);
    s.cdata = data';

    g = gifti(s);

function write_gifti(fn, g, opt)
    if isfield(opt, 'encoding')
        args = {opt.encoding};
    else
        args = {};
    end

    save(g, fn, args{:});

function s = build_niml_dset(ds)
    [data, node_indices] = get_surface_data_and_node_indices(ds);

    s = struct();
    s.node_indices = node_indices - 1; % base 1 -> base 0
    s.data = data';

    if isfield(ds, 'sa')
        if isfield(ds.sa, 'labels')
            s.labels = ds.sa.labels;
        end

        if isfield(ds.sa, 'stats')
            s.stats = ds.sa.stats;
        end
    end

function write_niml_dset(fn, s, opt)
    if isfield(opt, 'encoding')
        args = {opt.encoding};
    else
        args = {};
    end
    afni_niml_writesimple(s, fn, args{:});

function s = build_bv_smp(ds)
    [nsamples, nfeatures] = size(ds.samples);
    [data, node_indices] = get_surface_data_and_node_indices(ds);
    if ~isequal(node_indices, 1:nfeatures)
        error(['BrainVoyager smp only support data with all node '...
               'present and with .a.fdim.values{1}=1:N, where N '...
               'is the number of nodes']);
    end

    stats = cosmo_statcode(ds, 'bv');

    maps = cell(1, nsamples);
    for k = 1:nsamples
        t = xff('new:smp');
        map = t.Map;
        t.ClearObject();
        map.SMPData = data(k, :);

        if k == 1
            % store the number of features
            % note: unlike the niml_dset implementation, data is stored
            % for all nodes, even if some node indices did not have a value
            % originally
            nfeatures = numel(map.SMPData);
        end

        if isfield(ds, 'sa')
            if isfield(ds.sa, 'labels')
                map.Name = ds.sa.labels{k};
            end

            if ~isempty(stats) && ~isempty(stats{k})
                map = cosmo_structjoin(map, stats{k});
            end
        end

        if ~isempty(stats) && ~isempty(stats{k})
            map = cosmo_structjoin(map, stats{k});
        end

        map.BonferroniValue = nfeatures;

        maps{k} = map;
    end

    s = xff('new:smp');
    s.Map = cat(2, maps{:});
    s.NrOfMaps = nsamples;
    s.NrOfVertices = nfeatures;

    neuroelf_bless_wrapper(s);

function write_bv_smp(fn, s, opt)
    s.SaveAs(fn);

function result = neuroelf_bless_wrapper(arg)
    % deals with recent neuroelf (>v1.1), where bless is deprecated
    s = warning('off', 'neuroelf:xff:deprecated');
    resetter = onCleanup(@()warning(s));

    result = bless(arg);