function crossed_nbrhood=cosmo_cross_neighborhood(ds, nbrhoods, varargin)
% cross neighborhoods along dataset dimensions
%
% crossed_nbrhood=cosmo_cross_neighborhood(ds,nbrhoods,...)
%
% Inputs:
% ds dataset struct
% nbrhoods 1xK cell with neighborhood structs. Each element can be
% the output from cosmo_spherical_neighborhood,
% cosmo_meeg_chan_neighborhood,
% cosmo_surficial_neighborhood, or
% cosmo_interval_neighborhood.
% 'progress',p if p is true, then progress is shown
%
% Returns:
% crossed_nbrhood neighborhood struct with fields .[f]a and .neighbors,
% constructed by intersecting the neighborhoods from the
% input.
%
% Example:
% % Illustrate neighborhood by crossing freq and time, with freq
% % 5 bins wide and time 3 bins wide. Each neighborhood contains all
% % the channels, repeated up to 5*3=15 times (fewer at the border)
% ds=cosmo_synthetic_dataset('type','timefreq','size','big');
% freq_nbrhood=cosmo_interval_neighborhood(ds,'freq','radius',3);
% time_nbrhood=cosmo_interval_neighborhood(ds,'time','radius',5);
% nbrhood=cosmo_cross_neighborhood(ds, {freq_nbrhood, time_nbrhood},...
% 'progress',false);
% cosmo_disp(nbrhood.a.fdim)
% %|| .values
% %|| { [ 2 4 6 ... 10 12 14 ]@1x7
% %|| [ -0.2 -0.15 -0.1 -0.05 0 ] }
% %|| .labels
% %|| { 'freq'
% %|| 'time' }
% cosmo_disp(nbrhood.fa)
% %|| .freq
% %|| [ 1 2 3 ... 5 6 7 ]@1x35
% %|| .time
% %|| [ 1 1 1 ... 5 5 5 ]@1x35
% cosmo_disp(nbrhood.neighbors)
% %|| { [ 1 2 3 ... 9.79e+03 9.79e+03 9.79e+03 ]@1x6120
% %|| [ 1 2 3 ... 1.01e+04 1.01e+04 1.01e+04 ]@1x7650
% %|| [ 1 2 3 ... 1.04e+04 1.04e+04 1.04e+04 ]@1x9180
% %|| :
% %|| [ 307 308 309 ... 1.07e+04 1.07e+04 1.07e+04 ]@1x9180
% %|| [ 613 614 615 ... 1.07e+04 1.07e+04 1.07e+04 ]@1x7650
% %|| [ 919 920 921 ... 1.07e+04 1.07e+04 1.07e+04 ]@1x6120 }@35x1
%
% See also: cosmo_spherical_neighborhood, cosmo_meeg_chan_neighborhood,
% cosmo_interval_neighborhood
%
% # For CoSMoMVPA's copyright information and license terms, #
% # see the COPYING file distributed with CoSMoMVPA. #
cosmo_check_dataset(ds);
check_nbrhoods(nbrhoods,ds);
default.progress=1000;
opt=cosmo_structjoin(default, varargin{:});
ndim=numel(nbrhoods);
dims=struct();
dims.nbrs=cell(1,ndim);
dims.labels=cell(1,ndim);
dims.values=cell(1,ndim);
dims.fa=cell(1,ndim);
for k=1:ndim
nbrhood=ensure_sorted(nbrhoods{k});
dims.nbrs{k}=nbrhood.neighbors;
dims.values{k}=nbrhood.a.fdim.values(:);
dims.labels{k}=nbrhood.a.fdim.labels(:);
dims.fa{k}=nbrhood.fa;
end
% merge labels and values
dim_labels=cat(1,dims.labels{:});
dim_values=cat(1,dims.values{:});
ds_labels=ds.a.fdim.labels;
% check labels
check_labels(dim_labels,ds_labels);
% optimization: compute conjunctions differently in Matlab and Octave
is_matlab=cosmo_wtf('is_matlab');
% compute conjunctions of neighborhoods
[nbr_idxs, nbr_map_idxs]=conj_indices(dims.nbrs, ...
opt.progress, is_matlab);
% slice feature attributes
fa_nbrs=cell(ndim,1);
for k=1:ndim
fa=dims.fa{k};
fa_nbrs{k}=cosmo_slice(fa,nbr_map_idxs(k,:),2,'struct');
end
crossed_nbrhood=struct();
crossed_nbrhood.neighbors=nbr_idxs;
crossed_nbrhood.fa=cosmo_structjoin(fa_nbrs);
crossed_nbrhood.a=ds.a;
crossed_nbrhood.a.fdim=struct();
crossed_nbrhood.a.fdim.values=dim_values;
crossed_nbrhood.a.fdim.labels=dim_labels;
origin=struct();
origin.a=ds.a;
origin.fa=ds.fa;
crossed_nbrhood.origin=origin;
function check_nbrhoods(nbrhoods,ds)
if ~iscell(nbrhoods)
error(['second argument must a be cell of the form '...
'{nbrhood1, nbrhood2, ...}, where each nbrhood* '...
'is a neighborhood structure']);
end
for k=1:numel(nbrhoods)
nbrhood=nbrhoods{k};
cosmo_check_neighborhood(nbrhood,ds);
end
function nbrhood=ensure_sorted(nbrhood)
% ensure everything is sorted, as the helper function
% 'conj_indices' requires that
for j=1:numel(nbrhood.neighbors)
if ~issorted(nbrhood.neighbors{j})
nbrhood.neighbors{j}=sort(nbrhood.neighbors{j});
end
end
function check_labels(dim_labels,ds_labels)
% ensure no duplicate or missing labels
if ~isequal(sort(dim_labels), unique(dim_labels)) && ...
~(isempty(dim_labels))
error('Duplicate dimension labels in %s', ...
cosmo_strjoin(dim_labels,','));
elseif ~all(cosmo_match(dim_labels, ds_labels))
delta=setdiff(dim_labels, ds_labels);
error('dimension label unknown in dataset: %s', delta{1});
end
show_warning_if_weird_dim_label_order(dim_labels);
function show_warning_if_weird_dim_label_order(dim_labels)
expected_order={'chan','freq','time'};
norder=numel(expected_order);
pos_in_ds=@(needle)find(cosmo_match(dim_labels,{needle}));
for i=1:(norder-1)
for j=(i+1):norder
pre_label=expected_order{i};
pre=pos_in_ds(pre_label);
post_label=expected_order{j};
post=pos_in_ds(post_label);
if ~isempty(pre) && ...
~isempty(post) && ...
pre>post
cosmo_warning(['dimension labels ''%s'' and ''%s'' are '...
'in an uncommon order, which may cause '...
'issues when exporting data to FieldTrip '...
'or EEGLAB. It is strongly adviced '...
'to change the order of the input '...
'neighborhoods so that dimension ''%s'''...
'preceeds dimension ''%s'''],...
pre_label,post_label,post_label,pre_label);
end
end
end
function [flat_idxs, map_idxs]=conj_indices(dim_idxs, show_progress, use_fast)
% computes conjunction indices
%
% Input:
% dim_idxs A NDIMx1 cell, each with X_v cells with indices
% As used in this function, dim_idxs{dim}{j} are the
% sorted indices with feature attribute for the
% dim-th value equal to j.
%
% Outputs:
% flat_idxs Nx1 cell values where N=prod(X_*), each of which
% has the linear indices of the neighbors of an output
% feature.
% map_idxs N*ndim matrix with values in the dim-th column
% in the range 1..X_dim. This can be used to index the
% values in flat_idxs through sub-indices.
ndim=numel(dim_idxs);
% consider first dimension ('head')
head=dim_idxs{1};
nhead=numel(head);
head_map=1:nhead;
if ndim==1
% done
flat_idxs=head;
map_idxs=head_map;
return
end
% compute indices for remaining dimensions ('tail'), using recursion
[tail, tail_map]=conj_indices(dim_idxs(2:end), false, use_fast);
ntail=numel(tail);
% allocate space for output
n=nhead*ntail;
flat_idxs=cell(n,1);
map_idxs=zeros(ndim,n);
if show_progress
prev_msg='';
clock_start=clock();
end
% combine the dimensions
pos=0;
for j=1:ntail
for k=1:nhead
pos=pos+1;
headk=head{k};
if use_fast
flat_idxs{pos}=fast_intersect(headk, tail{j});
else
flat_idxs{pos}=intersect(headk, tail{j});
end
map_idxs(1,pos)=head_map(k);
map_idxs(2:end,pos)=tail_map(:,j);
end
if show_progress
msg=sprintf('crossing neighborhoods');
prev_msg=cosmo_show_progress(clock_start,j/ntail,msg,prev_msg);
end
end
function xy=fast_intersect(x,y)
% finds the intersection between two vectors
%
% xy=fast_intersect(x,y)
%
% Inputs:
% x numeric vector with elements sorted.
% y " "
%
% Returns:
% xy numeric vector containing elements present in both x and y,
% without duplicates and with elements sorted.
%
% Notes:
% - this function runs in O(n) compared to O(n*log(n)) with
% n=max(numel(x),numel(y)) for the built-in function 'intersect',
% as that function sorts the input data first.
% - in matlab it runs a factor 2 or 3 faster
% - in Octave it is very very slow for large inputs
nx=numel(x);
ny=numel(y);
n=min(nx,ny); % maximum size possible for output
xy=zeros(1,n); % allocate space for output
pos=0; % last position where a value was stored in xy
xi=1; % position in x
yi=1; % position in y
while xi<=nx && yi<=ny
if x(xi)<y(yi)
xi=xi+1;
elseif x(xi)>y(yi)
yi=yi+1;
else % x(xi)==y(yi); keep the value
pos=pos+1;
xy(pos)=x(xi);
xi=xi+1;
yi=yi+1;
end
end
xy=xy(1:pos); % only keep stored elements