function test_suite = test_surficial_neighborhood
% tests for cosmo_surficial_neighborhood
%
% # For CoSMoMVPA's copyright information and license terms, #
% # see the COPYING file distributed with CoSMoMVPA. #
try % assignment of 'localfunctions' is necessary in Matlab >= 2016
test_functions=localfunctions();
catch % no problem; early Matlab versions can use initTestSuite fine
end
initTestSuite;
function test_surficial_neighborhood_surface_dijkstra
if cosmo_skip_test_if_no_external('surfing')
return;
end
warning_state=warning();
warning_resetter=onCleanup(@()warning(warning_state));
warning('off');
opt=struct();
opt.progress=false;
ds=cosmo_synthetic_dataset('type','surface');
[vertices,faces]=get_synthetic_surface();
% dijkstra neighborhood fixed number of voxels
args={{vertices,faces},'count',4,'metric','dijkstra',opt};
nh1=cosmo_surficial_neighborhood(ds,args{:});
assertFalse(isfield(nh1.a,'vol'));
assert_equal_cell(nh1.neighbors,{ [ 1 2 4 3 ]
[ 2 1 3 5 ]
[ 3 2 6 5 ]
[ 4 1 5 2 ]
[ 5 2 4 6 ]
[ 6 3 5 2 ] });
assertEqual(nh1.fa.radius,[2 1 sqrt(2) sqrt(2) 1 2]);
assertEqual(nh1.fa.node_indices,1:6);
check_area(vertices,faces,nh1);
args={{vertices,faces},'radius',2.5,'metric','dijkstra',opt};
nh2=cosmo_surficial_neighborhood(ds,args{:});
assert_equal_cell(nh2.neighbors,{[1 2 3 4 5];
[1 2 3 4 5 6];
[1 2 3 4 5 6];
[1 2 3 4 5 6];
[1 2 3 4 5 6];
[2 3 4 5 6]; });
assertEqual(nh2.fa.radius,[2,2,1+sqrt(2),1+sqrt(2),2,2]);
assertEqual(nh2.fa.node_indices,1:6);
check_partial_neighborhood(ds,nh2,args);
check_area(vertices,faces,nh2);
args{1}{1}([2,6],:)=NaN;
nh3=cosmo_surficial_neighborhood(ds,args{:});
assert_equal_cell(nh3.neighbors,{[1 4 5];
[];
[3 4 5];
[1 3 4 5];
[1 3 4 5];
[]; });
assertEqual(nh3.fa.radius,[2,NaN,1+sqrt(2),1+sqrt(2),2,NaN]);
check_partial_neighborhood(ds,nh3,args);
check_area(args{1}{:},nh3);
args{2}='count';
args{3}=3;
nh4=cosmo_surficial_neighborhood(ds,args{:});
assert_equal_cell(nh4.neighbors,{[1 4 5];
[];
[3 5 4];
[4 1 5];
[5 4 3];
[]; });
assertEqual(nh4.fa.radius,[2,NaN,1+sqrt(2),1,sqrt(2),NaN]);
check_area(args{1}{:},nh4);
args{1}{1}=vertices;
args{1}{1}([2,5],:)=NaN; % split in two surfaces
args{3}=2;
nh5=cosmo_surficial_neighborhood(ds,args{:});
assert_equal_cell(nh5.neighbors,{[1 4];
[];
[3 6];
[4 1];
[];
[6 3]; });
assertEqual(nh5.fa.radius,[1,NaN,1,1,NaN,1]);
check_area(args{1}{:},nh5);
% throw error when too many nodes asked for
aet=@(varargin)assertExceptionThrown(@()...
cosmo_surficial_neighborhood(varargin{:}),'');
args{2}='count';
args{3}=3;
aet(ds,args{:})
function test_surficial_neighborhood_surface_direct
if cosmo_skip_test_if_no_external('surfing')
return;
end
warning_state=warning();
warning_resetter=onCleanup(@()warning(warning_state));
warning('off');
opt=struct();
opt.progress=false;
ds=cosmo_synthetic_dataset('type','surface');
[vertices,faces]=get_synthetic_surface();
% direct neighborhood
args={{vertices,faces},'direct',true,opt};
nh3=cosmo_surficial_neighborhood(ds,args{:});
assert_equal_cell(nh3.neighbors,{ [ 1 2 4 ]
[ 2 3 1 4 5 ]
[ 3 2 5 6 ]
[ 4 1 2 5 ]
[ 5 3 6 2 4 ]
[ 6 3 5 ] });
assertElementsAlmostEqual(nh3.fa.radius,sqrt([1 2 2 2 2 1]));
check_partial_neighborhood(ds,nh3,args);
args{1}{1}([2 5],:)=NaN;
nh4=cosmo_surficial_neighborhood(ds,args{:});
assert_equal_cell(nh4.neighbors,{ [ 1 4 ]
[]
[ 3 6 ]
[ 1 4 ]
[]
[6 3] });
check_partial_neighborhood(ds,nh4,args);
function test_surficial_neighborhood_surface_geodesic
if cosmo_skip_test_if_no_external('fast_marching') || ...
cosmo_skip_test_if_no_external('surfing')
return;
end
warning_state=warning();
warning_resetter=onCleanup(@()warning(warning_state));
warning('off');
opt=struct();
opt.progress=false;
ds=cosmo_synthetic_dataset('type','surface');%,'size','normal');
[vertices,faces]=get_synthetic_surface();
args={{vertices,faces},'count',4,opt};
nh=cosmo_surficial_neighborhood(ds,args{:});
assert_equal_cell(nh.neighbors,{[ 1 2 4 5 ]
[ 2 1 3 5 ]
[ 3 2 6 5 ]
[ 4 1 5 2 ]
[ 5 2 4 6 ]
[ 6 3 5 2 ] });
assertEqual(nh.fa.node_indices,1:6);
assertEqual(nh.fa.radius,[sqrt(.5)+1 1 sqrt(2) sqrt(2) 1 sqrt(.5)+1]);
vertices2=[NaN NaN NaN; vertices;NaN NaN NaN];
faces2=[faces+1; 1 1 8];
args={{vertices2,faces2},'count',4,opt};
nh2=cosmo_surficial_neighborhood(ds,args{:});
assertEqual(nh2.neighbors,{ zeros(1,0)
[ 2 3 5 6 ]
[ 3 2 4 6 ]
[ 4 3 6 2 ]
[ 5 2 6 3 ]
[ 6 3 5 4 ] });
function test_surficial_neighborhood_volume_geodesic
if cosmo_skip_test_if_no_external('fast_marching') || ...
cosmo_skip_test_if_no_external('surfing')
return;
end
warning_state=warning();
warning_resetter=onCleanup(@()warning(warning_state));
warning('off');
opt=struct();
opt.progress=false;
ds=cosmo_synthetic_dataset();
vertices=[-2 0 2 -2 0 2;...
-1 -1 -1 1 1 1;...
-1 -1 -1 -1 -1 -1]';
faces= [ 3 2 3 2;...
2 1 5 4;...
5 4 6 5 ]';
pial=vertices;
pial(:,3)=pial(:,3)+1;
white=vertices;
white(:,3)=white(:,3)-1;
nh1=cosmo_surficial_neighborhood(ds,{vertices,[-1 1],faces},...
'count',4,opt);
nh2=cosmo_surficial_neighborhood(ds,{pial,white,faces},...
'count',4,opt);
assert_equal_cell(nh1.neighbors,{[ 1 2 4 5 ]
[ 1 2 3 5 ]
[ 2 3 5 6 ]
[ 4 1 5 2 ]
[ 5 2 4 6 ]
[ 6 3 5 2 ] });
assertEqual(nh1.fa.node_indices,1:6);
assertEqual(nh1,nh2);
function test_surficial_neighborhood_volume_dijkstra
if cosmo_skip_test_if_no_external('surfing')
return;
end
warning_state=warning();
warning_resetter=onCleanup(@()warning(warning_state));
warning('off');
opt=struct();
opt.progress=false;
ds=cosmo_synthetic_dataset();
vertices=[-2 0 2 -2 0 2;
-1 -1 -1 1 1 1
-1 -1 -1 -1 -1 -1]';
faces= [ 3 2 3 2
2 1 5 4
5 4 6 5 ]';
pial=vertices;
pial(:,3)=pial(:,3)+1;
white=vertices;
white(:,3)=white(:,3)-1;
args3={{vertices,[-1 1],faces},'metric','dijkstra','count',4,opt};
args4={{pial,white,faces},'metric','dijkstra','count',4,opt};
nh3=cosmo_surficial_neighborhood(ds,args3{:});
nh4=cosmo_surficial_neighborhood(ds,args4{:});
assert_equal_cell(nh3.neighbors,{ [ 1 2 4 3 ]
[ 2 1 3 5 ]
[ 3 2 6 5 ]
[ 4 1 5 2 ]
[ 5 2 4 6 ]
[ 6 3 5 2 ] });
assertEqual(nh3.fa.node_indices,1:6);
assert_equal_cell(nh4.neighbors,nh3.neighbors);
assertFalse(isfield(nh3.a,'vol'));
% TODO
% check_partial_neighborhood(ds,nh3,args3);
% check_partial_neighborhood(ds,nh3,args3);
function test_surficial_neighborhood_exceptions
if cosmo_skip_test_if_no_external('surfing')
return
end
ds=cosmo_synthetic_dataset('type','surface');%,'size','normal');
[vertices,faces]=get_synthetic_surface();
aet=@(varargin)assertExceptionThrown(@()...
cosmo_surficial_neighborhood(varargin{:},...
'progress',false),'');
aet(ds,{vertices,faces});
% need surfaces
aet(ds,{},'radius',2);
% center_ids not supported for surface dataset
aet(ds,{vertices,faces},'radius',2,'center_ids',1);
% cannot have duplicate feature ids
ds_double=cosmo_stack({ds,ds},2);
ds_double.a.fdim.values{1}=[1:6,1:6];
aet(ds_double,{vertices,faces},'radius',2);
% outside range
ds_double.a.fdim.values{1}=[1:5,1:5];
aet(ds_double,{vertices,faces},'radius',2);
% cannot have fmri and surface dataset combined
ds2=cosmo_synthetic_dataset();
ds2.a.fdim.values{end+1}=ds.a.fdim.values{1};
ds2.a.fdim.labels{end+1}=ds.a.fdim.labels{1};
ds2.fa.node_indices=ds.fa.node_indices;
aet(ds2,{vertices,faces},'radius',2);
% cannot have MEEG dataset
ds_meeg=cosmo_synthetic_dataset('type','meeg');
aet(ds_meeg,{vertices,faces},'radius',2);
% need positive scalar radius
aet(ds,{vertices,faces},'radius',-1);
aet(ds,{vertices,faces},'radius',eye(2));
function check_partial_neighborhood(ds,nh,args)
% see if when have a partial dataset, the neighborbood reflects
% that too
nf=size(ds.samples,2);
rp=randperm(nf);
keep_count=round(nf*.7);
keep_sel=rp(1:keep_count);
keep_all=[keep_sel keep_sel keep_sel];
ds_sel=cosmo_slice(ds,keep_all,2);
fdim=ds_sel.a.fdim.values{1};
rp_fdim=randperm(numel(fdim));
ds_sel.a.fdim.values{1}=fdim(rp_fdim);
nh_sel=cosmo_surficial_neighborhood(ds_sel,args{:});
assertEqual(numel(nh_sel.neighbors), numel(keep_sel));
assertEqual(nh_sel.a.fdim.labels,nh.a.fdim.labels);
assertEqual(nh_sel.a.fdim.values{1},nh.a.fdim.values{1}(rp_fdim));
assertEqual(numel(nh_sel.a.fdim.values),numel(nh.a.fdim.values));
assertEqual(ds_sel.a,nh_sel.a);
opt=cosmo_structjoin(args(2:end));
if isfield(opt,'radius')
metric=opt.metric;
metric_arg=opt.radius;
elseif isfield(opt,'count')
metric=opt.metric;
metric_arg=[10 opt.count];
elseif isfield(opt,'direct')
metric='direct';
if opt.direct
metric_arg=NaN;
else
metric_arg=0;
end
else
assert(false);
end
faces=args{1}{2};
n2f=surfing_invertmapping(faces);
nodes_ds_sel=ds_sel.a.fdim.values{1}(ds_sel.fa.node_indices);
nodes_ds=ds.a.fdim.values{1}(ds.fa.node_indices);
nodes_nh_sel=nh_sel.a.fdim.values{1}(nh_sel.fa.node_indices);
vertices=args{1}{1};
nvertices=size(vertices,1);
nodes_kept=cosmo_match(1:nvertices,nodes_ds_sel);
vertices(~nodes_kept,:)=NaN;
node_mask=all(isfinite(vertices),2);
nodes_removed=setdiff(nodes_ds(:)',nodes_ds_sel(:)');
assertEqual(setxor(nodes_removed,nodes_ds_sel),1:nf);
nb_sel=nh_sel.neighbors;
for k=1:numel(nh_sel.neighbors)
sel_center_node=nodes_nh_sel(k);
idx=find(nodes_ds==sel_center_node);
assert(numel(idx)==1);
center_node=nodes_ds(idx);
assertEqual(sel_center_node, center_node);
switch metric
case 'direct'
if node_mask(sel_center_node)
direct_neighbors=surfing_surface_nbrs(faces',...
vertices');
around_nodes=direct_neighbors(sel_center_node,:);
msk=cosmo_match(around_nodes, ...
find(isfinite(vertices(:,1))));
% add node itself
around_nodes=[sel_center_node,...
around_nodes(msk & around_nodes>0)];
else
around_nodes=[];
end
otherwise
around_nodes=surfing_circleROI(vertices',faces',...
sel_center_node,metric_arg,metric,n2f);
end
sel_around_nodes=nodes_ds_sel(nb_sel{k});
if isempty(sel_around_nodes)
assertTrue(isempty(around_nodes));
else
assertEqual(unique(sel_around_nodes),...
setdiff(around_nodes, nodes_removed))
end
end
function test_surface_subsampling
if cosmo_skip_test_if_no_external('surfing')
return;
end
vertices=[0 -1 -2 -1 1 2 1 3 4 3;
0 -2 0 2 2 0 -2 2 0 -2;
0 0 0 0 0 0 0 0 0 0]';
faces=[1 1 1 1 1 1 5 8 6 6;
2 3 4 5 6 7 8 9 9 10;
3 4 5 6 7 2 6 6 10 7]';
% make custom volume
ds=cosmo_synthetic_dataset('size','small');
cp=cosmo_cartprod({1:7,1:3})';
ds.fa.i=cp(1,:);
ds.fa.j=cp(2,:);
ds.fa.k=ds.fa.i*0+1;
ds.a.fdim.values={1:7;1:3;1};
ds.samples=zeros(numel(ds.sa.targets),numel(ds.fa.i));
ds.a.vol.dim=cellfun(@numel,ds.a.fdim.values)';
ds.a.vol.mat(2,4)=-4;
ds.a.vol.mat(3,4)=-1;
ds.a.vol.mat(1,1)=1;
ds.a.vol.mat(2,2)=2;
ds.a.vol.mat(3,3)=1;
surfs={vertices,faces,[-4 5]};
opt=struct();
opt.progress=false;
opt.radius=3;
opt.metric='euclidean';
nh=cosmo_surficial_neighborhood(ds,surfs,opt);
assertEqual(nh.neighbors,{ [ 2 4 8 10 12 16 18 ]
[ 2 4 8 10 ]
[ 2 8 10 16 ]
[ 8 10 16 18 ]
[ 10 12 16 18 20 ]
[ 4 6 10 12 14 18 20 ]
[ 2 4 6 10 12 ]
[ 12 14 18 20 ]
[ 6 12 14 20 ]
[ 4 6 12 14 ] });
assertEqual(nh.origin.fa,ds.fa);
assertEqual(nh.origin.a,ds.a);
% test subsampling
subsample=2;
surfs={vertices,faces,[-4 5],subsample};
nh2=cosmo_surficial_neighborhood(ds,surfs,opt);
assertEqual(nh2.neighbors,{ [ 2 4 8 10 ]
[ 2 8 10 16 ]
[ 8 10 16 18 ]
[ 10 12 16 18 20 ]
[ 2 4 6 10 12 ]
[ 12 14 18 20 ]
[ 6 12 14 20 ]
[ 4 6 12 14 ] });
assertEqual(nh2.origin.fa,ds.fa);
assertEqual(nh2.origin.a,ds.a);
% subsampling with pial surface
pial=bsxfun(@plus,vertices,[0 0 1]);
white=bsxfun(@plus,vertices,[0 0 -1]);
[vo,fo]=surfing_subsample_surface(vertices,faces,2,.2,0);
surfs={pial,white,faces,vo,fo};
nh3=cosmo_surficial_neighborhood(ds,surfs,opt);
assertEqual(nh2,nh3);
% check center ids options
slice_ids=[5 3 2];
nh4=cosmo_surficial_neighborhood(ds,surfs,opt,'center_ids',slice_ids);
nh4_sl=struct();
nh4_sl.neighbors=nh3.neighbors(slice_ids);
nh4_sl.fa=cosmo_slice(nh3.fa,slice_ids,2,'struct');
nh4_sl.a=nh3.a;
assertEqual(nh4.a.fdim.values{1}(nh4.fa.node_indices),...
nh4_sl.a.fdim.values{1}(nh4_sl.fa.node_indices));
assertEqual(nh4.neighbors,nh4_sl.neighbors);
% try with file names
fn_pial=cosmo_make_temp_filename('pial','.asc');
fn_white=cosmo_make_temp_filename('white','.asc');
fn_tiny=cosmo_make_temp_filename('tiny','.asc');
cleaner1=onCleanup(@()delete(fn_pial));
cleaner2=onCleanup(@()delete(fn_white));
cleaner3=onCleanup(@()delete(fn_tiny));
surfing_write(fn_pial, pial, faces);
surfing_write(fn_white, white, faces);
surfing_write(fn_tiny, vo, fo);
surfs={fn_pial,fn_white,fn_tiny};
nh5=cosmo_surficial_neighborhood(ds,surfs,opt);
assertEqual(nh2,nh5);
% should work with alternative voldef
ds_bad_vol=ds;
ds_bad_vol.a.vol.mat(:)=NaN;
ds_bad_vol.a.vol.dim(:)=NaN;
nh6=cosmo_surficial_neighborhood(ds_bad_vol,surfs,opt,...
'vol_def',ds.a.vol);
nh6.origin.a.vol=ds.a.vol;
assertEqual(nh5,nh6);
% check exceptions
aet=@(varargin)assertExceptionThrown(@()...
cosmo_surficial_neighborhood(varargin{:}),'');
surfs={fn_pial,fn_pial,fn_tiny};
aet(ds,surfs,opt);
white_bad=white;
white_bad=white_bad(2:end,:);
aet(ds,{pial,white_bad,faces},opt);
% missing faces for output surface
aet(ds,{fn_pial,fn_white,vo},opt);
% face mismatch
faces_bad=faces;
faces_bad=faces_bad(end:-1:1,:);
surfing_write(fn_white, white, faces_bad);
aet(ds,{fn_pial,fn_white},opt);
% too many surf arguments
aet(ds,{fn_pial,fn_white,fn_tiny,fn_tiny},opt);
aet(ds,{pial,white,faces,pial,white,white},opt);
aet(ds,{pial,white,faces,fn_pial,white},opt);
% surfs are not a cell
aet(ds,struct,opt);
aet(ds,{pial,white,{}});
function check_area(vertices,faces,nh)
assert(isfield(nh.fa,'area'));
area=surfing_surfacearea(vertices,faces);
node_idxs=nh.fa.node_indices;
n_nodes=numel(node_idxs);
for k=1:n_nodes
nbr_idxs=node_idxs(nh.neighbors{k});
expected_area=sum(area(nbr_idxs));
if isnan(expected_area)
assertEqual(expected_area,nh.fa.area(k))
else
assertElementsAlmostEqual(expected_area,nh.fa.area(k));
end
end
function [vertices,faces]=get_synthetic_surface()
% return the following surface (face indices in [brackets])
%
% 1-----2-----3
% | /| /|
% |[2]/ |[1]/ |
% | / | / |
% | /[4]| /[3]|
% |/ |/ |
% 4-----5-----6
vertices=[0 0 0 1 1 1;
1 2 3 1 2 3;
0 0 0 0 0 0]';
faces= [ 3 2 3 2
2 1 5 4
5 4 6 5 ]';
function assert_equal_cell(x,y)
% small helper
assertEqual(size(x),size(y))
for k=1:numel(x)
xk=x{k};
yk=y{k};
if isempty(xk)
assertTrue(isempty(yk));
else
assertEqual(sort(xk),sort(yk));
end
end