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