cosmo cartprod

function p=cosmo_cartprod(xs, convert_to_numeric)
% returns the cartesian product with all combinations of the input
%
% p=cosmo_cartprod(xs[, convert_to_numeric])
%
% Inputs:
%   xs                   Px1 cell array with values for which the product
%                        is to be returned. Each element xs{k} should be
%                        - a cell with Qk values
%                        - a numeric array [xk_1,...,xk_Qk], which is
%                          interpreted as the cell {xk_1,...,xk_Qk}.
%                        - or a string s, which is interpreted as {s}.
%                        Alternatively xs can be a struct with P fieldnames
%                        where each value is a cell with Qk values.
%
%   convert_to_numeric   Optional; if true (default), then when the output
%                        contains numeric values only a numerical matrix is
%                        returned; otherwise a cell is returned.
% Output:
%   p                    QxP cartesian product of xs (where Q=Q1*...*Qk)
%                        containing all combinations of values in xs.
%                        - If xs is a cell, then p is represented by either
%                          a matrix (if all values in xs are numeric and
%                          convert_to_numeric==true) or a cell (in all
%                          other cases).
%                        - If xs is a struct, then p is a Qx1 cell. Each
%                          element in p is a struct with the same
%                          fieldnames as xs.
%
% Examples:
%     cosmo_cartprod({{1,2},{'a','b','c'}})'
%     %|| {1,2,1,2,1,2;
%     %|| 'a','a' ,'b','b','c','c'}
%
%     cosmo_cartprod({[1,2],[5,6,7]})'
%     %|| [1,2,1,2,1,2;
%     %||  5,5,6,6,7,7]
%
%     cosmo_cartprod(repmat({1:2},1,4))'
%     %|| [1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2;
%     %||  1 1 2 2 1 1 2 2 1 1 2 2 1 1 2 2;
%     %||  1 1 1 1 2 2 2 2 1 1 1 1 2 2 2 2;
%     %||  1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2]
%
%     s=struct();
%     s.roi={'v1','loc'};
%     s.hemi={'L','R'};
%     s.subj=[1 3 9];
%     s.ana='vis';
%     s.beta=4;
%     p=cosmo_cartprod(s)';
%     cosmo_disp(p);
%     %|| { .roi     .roi     .roi    ... .roi     .roi     .roi
%     %||     'v1'     'loc'    'v1'        'loc'    'v1'     'loc'
%     %||   .hemi    .hemi    .hemi       .hemi    .hemi    .hemi
%     %||     'L'      'L'      'R'         'L'      'R'      'R'
%     %||   .subj    .subj    .subj       .subj    .subj    .subj
%     %||     [ 1 ]    [ 1 ]    [ 1 ]       [ 9 ]    [ 9 ]    [ 9 ]
%     %||   .ana     .ana     .ana        .ana     .ana     .ana
%     %||     'vis'    'vis'    'vis'       'vis'    'vis'    'vis'
%     %||   .beta    .beta    .beta       .beta    .beta    .beta
%     %||     [ 4 ]    [ 4 ]    [ 4 ]       [ 4 ]    [ 4 ]    [ 4 ] }@1x12
%     %
%
% #   For CoSMoMVPA's copyright information and license terms,   #
% #   see the COPYING file distributed with CoSMoMVPA.           #

    if nargin<2, convert_to_numeric=true; end

    as_struct=isstruct(xs);

    if as_struct
        % input is a struct; put the values in each field in a cell.
        [xs,fns]=struct2cell(xs);
    elseif ~iscell(xs)
        error('Unsupported input: expected a cell or struct');
    end

    if isempty(xs)
        p=cell(1,0);
        return
    end

    p=cartprod(xs);

    % if input was a struct, output is a cell with structs
    if as_struct
        p=cell2structs(p, fns);
    elseif convert_to_numeric && ~isempty(p) && ...
                        all(cellfun(@isnumeric,p(:)))
        % all values are numeric; convert to numeric matrix
        p=reshape([p{:}],size(p));
    end

function p=cartprod(xs)

    ndim=numel(xs);

    % get values in first dimension (the 'head')
    xhead=xs{1};
    if isnumeric(xhead) || islogical(xhead)
        % put numeric arrays in a cell
        xhead=num2cell(xhead);
    elseif ischar(xhead)
        xhead={xhead};
    end

    % ensure head is a column vector
    xhead=xhead(:);

    if ndim==1
        p=xhead;
    else
        % use recursion to find cartprod of remaining dimensions
        % (the 'tail')
        xtail=xs(2:end);
        ptail=cartprod(xtail); % ensure output is always a cell

        % get sizes of head and tail
        nhead=numel(xhead);
        ntail=size(ptail,1);

        % allocate space for output
        rows=cell(ntail,1);
        for k=1:ntail
            % merge head and tail
            % ptailk_rep is a repeated version of the k-th tail row
            % to match the number of rows in head
            ptailk_rep=repmat(ptail(k,:),nhead,1);
            rows{k}=cat(2,xhead,ptailk_rep);
        end

        % stack the rows vertically
        p=cat(1,rows{:});
    end



function [c,fns]=struct2cell(xs)
    fns=fieldnames(xs);
    ndim=numel(fns);
    c=cell(1,ndim); % space for values in each dimension
    for k=1:ndim
        c{k}=xs.(fns{k});
    end

function struct_cell=cell2structs(p, fns)
    % number of output
    n=size(p,1);
    ndim=numel(fns);

    % allocate space for structs
    struct_cell=cell(n,1);

    % set values for each struct
    for k=1:n
        s=struct();
        for j=1:ndim
            % use the same fieldnames as in the input
            s.(fns{j})=p{k,j};
        end
        struct_cell{k}=s;
    end