function test_suite=test_montecarlo_cluster_stat
% tests for cosmo_montecarlo_cluster_stat
%
% # 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_onesample_ttest_montecarlo_cluster_extremes
ds=cosmo_synthetic_dataset('ntargets',1,...
'nchunks',20,...
'size','normal');
nh=cosmo_cluster_neighborhood(ds,'progress',false);
z_lookup=get_zscore_lookup_table();
for repeat=1:5
for signal_sign=[-1 1]
magnitude=10;
ds_pos=ds;
ds_pos.samples=randn(size(ds.samples))+signal_sign*magnitude;
niter=10+ceil(rand()*(numel(z_lookup)-11));
opt.niter=niter;
opt.h0_mean=0;
opt.dh=.25;
opt.progress=false;
z_ds=cosmo_montecarlo_cluster_stat(ds_pos,nh,opt);
z_expected=signal_sign*z_lookup(niter+1);
nfeatures=size(ds.samples,2);
assertElementsAlmostEqual(z_ds.samples,...
z_expected+zeros(1,nfeatures),...
'absolute',1e-4);
end
end
function z_lookup=get_zscore_lookup_table()
% table generated using
% niter=1:40; fprintf('%.4f %.4f %.4f %.4f %.4f ...\n',...
% norminv(1-1./niter))
% but without need for presence of norminv
z_lookup=[ -Inf 0.0000 0.4307 0.6745 0.8416 ...
0.9674 1.0676 1.1503 1.2206 1.2816 ...
1.3352 1.3830 1.4261 1.4652 1.5011 ...
1.5341 1.5647 1.5932 1.6199 1.6449 ...
1.6684 1.6906 1.7117 1.7317 1.7507 ...
1.7688 1.7862 1.8027 1.8186 1.8339 ...
1.8486 1.8627 1.8764 1.8895 1.9022 ...
1.9145 1.9264 1.9379 1.9491 1.9600 ...
];
function test_onesample_ttest_mccs_left_tail
helper_test_mccs_with_tail(true,false);
function test_onesample_ttest_mccs_right_tail
helper_test_mccs_with_tail(false,true);
function test_onesample_ttest_mccs_both_tail
helper_test_mccs_with_tail(true,true);
function test_onesample_ttest_mccs_no_tail
helper_test_mccs_with_tail(false,false);
function helper_test_mccs_with_tail(left_tail,right_tail)
nchunks=ceil(rand()*5)+20;
ds=cosmo_synthetic_dataset('size','big',...
'nchunks',nchunks,...
'ntargets',1);
ds=cosmo_slice(ds,ds.fa.i<8,2);
ds=cosmo_dim_prune(ds);
nfeatures=size(ds.samples,2);
% generate some effect for some of the features. Here we generate data
% where about 1/3rd of the features may show an effect in the left tail
% (less than 0), and 1/3rd of the features may show an effect in the
% right tail (greater than 0).
ratio_effect=1/3;
nfeatures_effect=ceil(nfeatures*ratio_effect);
z_table=get_zscore_lookup_table();
[unused,rp]=sort(rand(1,nfeatures));
ds.samples=randn(nchunks,nfeatures);
sigma=4/sqrt(nchunks);
% add effect below zero
if left_tail
neg_idx=rp(nfeatures_effect+(1:nfeatures_effect));
ds.samples(:,neg_idx)=ds.samples(:,neg_idx)-sigma;
end
% add effect above zero
if right_tail
pos_idx=rp(1:nfeatures_effect);
ds.samples(:,pos_idx)=ds.samples(:,pos_idx)+sigma;
end
% run TFCE
nh=cosmo_cluster_neighborhood(ds,'progress',false);
opt=struct();
opt.niter=ceil(rand()*10+10);
opt.h0_mean=0;
opt.dh=1; % make it faster
opt.progress=false;
z=cosmo_montecarlo_cluster_stat(ds,nh,opt);
% check sign of output of each feature
t=cosmo_stat(ds,'t');
for tail_sign=[-1,1]
% everywhere where z is positive [negative], the
% corresponding t value must also be positive [negative]
z_msk=z.samples*tail_sign>0;
t_msk=t.samples*tail_sign>0;
assertTrue(all(t_msk(z_msk)));
end
% see how many significant effects found
tiny=1e-4;
for tail_sign=[-1,1]
has_effect=(tail_sign==-1 && left_tail) || ...
(tail_sign==1 && right_tail);
if has_effect
% quite a lof of features should show an effect
minimum_tail_ratio=.1;
maximum_tail_ratio=.5;
else
% very few features should show an effect
minimum_tail_ratio=0;
maximum_tail_ratio=.1;
end
expected_extreme_z=tail_sign*z_table(opt.niter+1);
if has_effect
extreme_z=max(z.samples*tail_sign)*tail_sign;
assertElementsAlmostEqual(extreme_z,...
expected_extreme_z,...
'absolute',tiny);
end
% deal with rounding
extreme_ratio=mean(abs(z.samples-expected_extreme_z)<tiny);
assertTrue(extreme_ratio>=minimum_tail_ratio);
assertTrue(extreme_ratio<=maximum_tail_ratio);
end
% count number of features without an effect
median_zero_ratio=1-2*ratio_effect;
if ~left_tail
median_zero_ratio=median_zero_ratio+ratio_effect;
end
if ~right_tail
median_zero_ratio=median_zero_ratio+ratio_effect;
end
if left_tail && right_tail
% allow for more non-results
margin=1/4;
else
margin=1/6;
end
minimum_zero_ratio=median_zero_ratio-margin;
maximum_zero_ratio=median_zero_ratio+margin;
zero_ratio=mean(abs(z.samples)<tiny);
assertTrue(zero_ratio>=minimum_zero_ratio);
assertTrue(zero_ratio<=maximum_zero_ratio);
function test_onesample_ttest_montecarlo_cluster_stat_basics
ds=cosmo_synthetic_dataset('ntargets',1,'nchunks',6);
nh=cosmo_cluster_neighborhood(ds,'progress',false);
opt=struct();
opt.niter=9;
opt.h0_mean=0;
opt.seed=7;
opt.progress=false;
z=cosmo_montecarlo_cluster_stat(ds,nh,opt);
assertElementsAlmostEqual(z.samples,...
[0.8416 0 1.2816 0 1.2816 0],...
'absolute',1e-4);
function test_onesample_ttest_montecarlo_cluster_stat_other_mean
ds=cosmo_synthetic_dataset('ntargets',1,'nchunks',6);
nh=cosmo_cluster_neighborhood(ds,'progress',false,'fmri',1);
% different h0_mean
opt=struct();
opt.niter=9;
opt.seed=7;
opt.progress=false;
opt.h0_mean=1.2;
z=cosmo_montecarlo_cluster_stat(ds,nh,opt);
assertElementsAlmostEqual(z.samples,...
[0 -1.2816 1.2816 -1.2816 0.25335 -1.2816],...
'absolute',1e-4);
function test_onesample_ttest_montecarlo_cluster_stat_other_dh
ds=cosmo_synthetic_dataset('ntargets',1,'nchunks',6);
nh=cosmo_cluster_neighborhood(ds,'progress',false,'fmri',1);
opt=struct();
opt.niter=9;
opt.h0_mean=1.2;
opt.seed=7;
opt.progress=false;
opt.dh=1.1;
z=cosmo_montecarlo_cluster_stat(ds,nh,opt);
assertElementsAlmostEqual(z.samples,...
[0 -1.2816 0 -0.5244 0 -0.5244],...
'absolute',1e-4);
function test_onesample_ttest_montecarlo_cluster_stat_exceptions
ds=cosmo_synthetic_dataset('ntargets',1,'nchunks',6);
nh2=cosmo_cluster_neighborhood(ds,'progress',false,'fmri',1);
aet=@(varargin)assertExceptionThrown(@()...
cosmo_montecarlo_cluster_stat(varargin{:}),'');
opt=struct();
opt.niter=9;
opt.h0_mean=1.2;
opt.seed=7;
opt.progress=false;
opt.dh=1.1;
opt.cluster_stat='maxsize';
aet(ds,nh2,opt);
opt=rmfield(opt,'dh');
aet(ds,nh2,opt);
opt.p_uncorrected=.55;
aet(ds,nh2,opt);
opt.p_uncorrected=.4;
opt.h0_mean=0;
z_ds4=cosmo_montecarlo_cluster_stat(ds,nh2,opt);
assertElementsAlmostEqual(z_ds4.samples,...
[0.5244 0 0 0.5244 0.5244 0],...
'absolute',1e-4);
opt.h0_mean=2;
z_ds5=cosmo_montecarlo_cluster_stat(ds,nh2,opt);
assertElementsAlmostEqual(z_ds5.samples,...
[-0.25335 -0.25335 0 -0.25335 0 0],...
'absolute',1e-4);
opt=rmfield(opt,'h0_mean');
aet(ds,nh2,opt);
function test_onesample_ttest_montecarlo_cluster_stat_strong
ds=cosmo_synthetic_dataset('ntargets',1,'nchunks',12);
nh=cosmo_cluster_neighborhood(ds,'progress',false,'fmri',1);
% lots of signal, but in rare cases not all features show the maximum
% z score. instead we allow for a few failures
n_attempts = 5;
allowed_fail_ratio = 0.25;
n_fail=0;
n_pass=0;
for attempt=1:n_attempts
for effect_sign=[-1,1]
niter=ceil(rand()*10+20);
z_table=get_zscore_lookup_table();
expected_z=z_table(niter+1);
opt=struct();
opt.h0_mean=(-effect_sign)*15;
opt.progress=false;
opt.niter=niter;
z=cosmo_montecarlo_cluster_stat(ds,nh,opt);
max_delta = max(abs(effect_sign*expected_z - z.samples));
if max_delta > 1e-4
n_fail=n_fail+1;
else
n_pass=n_pass+1;
end
end
end
fail_ratio = n_fail / (n_fail+n_pass);
assertTrue(fail_ratio < allowed_fail_ratio);
function test_twosample_ttest_montecarlo_cluster_stat_basics
aet=@(varargin)assertExceptionThrown(@()...
cosmo_montecarlo_cluster_stat(varargin{:}),'');
ds=cosmo_synthetic_dataset('ntargets',2,'nchunks',6,'sigma',2);
nh1=cosmo_cluster_neighborhood(ds,'progress',false);
% test within-subjects
opt=struct();
opt.niter=9;
opt.seed=1;
opt.progress=false;
z=cosmo_montecarlo_cluster_stat(ds,nh1,opt);
assertElementsAlmostEqual(z.samples,...
[0.5244 -1.2816 0 0.84162 -0.25335 0],...
'absolute',1e-4);
function test_twosample_ttest_montecarlo_cluster_stat_ws
ds=cosmo_synthetic_dataset('ntargets',2,'nchunks',6,'sigma',2);
nh=cosmo_cluster_neighborhood(ds,'progress',false);
% test within-subjects
opt=struct();
opt.niter=9;
opt.seed=1;
opt.progress=false;
opt.cluster_stat='maxsum';
opt.p_uncorrected=0.35;
z=cosmo_montecarlo_cluster_stat(ds,nh,opt);
assertElementsAlmostEqual(z.samples,...
[0.5244 -1.2816 0 0.5244 -1.2816 0],...
'absolute',1e-4);
function test_twosample_ttest_montecarlo_cluster_stat_bs
% test between-subjects
ds=cosmo_synthetic_dataset('ntargets',2,'nchunks',6,'sigma',2);
nh=cosmo_cluster_neighborhood(ds,'progress',false);
ds.sa.chunks=ds.sa.chunks*2+ds.sa.targets;
opt=struct();
opt.niter=9;
opt.seed=1;
opt.progress=false;
z=cosmo_montecarlo_cluster_stat(ds,nh,opt);
assertElementsAlmostEqual(z.samples,...
[0 -1.2816 0 0 -1.2816 0],...
'absolute',1e-4);
function test_twosample_ttest_montecarlo_cluster_stat_strong
% test between-subjects
ds=cosmo_synthetic_dataset('ntargets',2,'nchunks',6,'sigma',2);
nh=cosmo_cluster_neighborhood(ds,'progress',false);
ds.sa.chunks=ds.sa.chunks*2+ds.sa.targets;
% lots of signal, but in rare cases not all features show the maximum
% z score. instead we allow for a few failures
n_attempts = 5;
allowed_fail_ratio = 0.25;
n_fail=0;
n_pass=0;
for attempt=1:n_attempts
% lots of signal, should work with no seed specified
opt=struct();
opt.niter=14 + ceil(rand()*5);
opt.progress=false;
msk1=ds.sa.targets==1;
ds.samples(msk1,:)=ds.samples(msk1,:)+10;
z=cosmo_montecarlo_cluster_stat(ds,nh,opt);
z_table=get_zscore_lookup_table();
expected_z=z_table(opt.niter+1);
max_delta = max(abs(expected_z - z.samples));
if max_delta > 1e-4
n_fail=n_fail+1;
else
n_pass=n_pass+1;
end
end
fail_ratio = n_fail / (n_fail+n_pass);
assertTrue(fail_ratio < allowed_fail_ratio);
function test_twosample_ttest_montecarlo_cluster_stat_exceptions
aet=@(varargin)assertExceptionThrown(@()...
cosmo_montecarlo_cluster_stat(varargin{:}),'');
% test between-subjects
ds=cosmo_synthetic_dataset('ntargets',2,'nchunks',6,'sigma',2);
nh1=cosmo_cluster_neighborhood(ds,'progress',false);
opt.cluster_stat='foo';
aet(ds,nh1,opt);
opt=rmfield(opt,'cluster_stat');
opt.h0_mean=3;
aet(ds,nh1,opt);
function test_anova_montecarlo_cluster_stat_ws
ds=cosmo_synthetic_dataset('ntargets',3,'nchunks',6,'sigma',2);
nh1=cosmo_cluster_neighborhood(ds,'progress',false);
% test within-subjects
opt=struct();
opt.niter=9;
opt.seed=1;
opt.progress=false;
z_ds1=cosmo_montecarlo_cluster_stat(ds,nh1,opt);
assertElementsAlmostEqual(z_ds1.samples,...
[1.2816 0 0 0 0.84162 0],...
'absolute',1e-4);
function test_anova_montecarlo_cluster_stat_bs
ds=cosmo_synthetic_dataset('ntargets',3,'nchunks',6,'sigma',2);
nh1=cosmo_cluster_neighborhood(ds,'progress',false);
% test within-subjects
opt=struct();
opt.niter=9;
opt.seed=1;
opt.progress=false;
% test between-subjects
ds.sa.chunks=ds.sa.chunks*3+ds.sa.targets;
z=cosmo_montecarlo_cluster_stat(ds,nh1,opt);
assertElementsAlmostEqual(z.samples,...
[1.2816 0.84162 0 0 1.2816 0.5244],...
'absolute',1e-4);
function test_anova_montecarlo_cluster_stat_exceptions
aet=@(varargin)assertExceptionThrown(@()...
cosmo_montecarlo_cluster_stat(varargin{:}),'');
ds=cosmo_synthetic_dataset('ntargets',3,'nchunks',6,'sigma',2);
nh=cosmo_cluster_neighborhood(ds,'progress',false);
opt=struct();
opt.niter=9;
opt.seed=1;
opt.progress=false;
% unbalanced design
ds.sa.chunks(1)=5;
aet(ds,nh,opt);
function test_null_data_montecarlo_cluster_stat
aet=@(varargin)assertExceptionThrown(@()...
cosmo_montecarlo_cluster_stat(varargin{:}),'');
ds=cosmo_synthetic_dataset('ntargets',1,'nchunks',6,'sigma',2);
nh1=cosmo_cluster_neighborhood(ds,'progress',false);
n_null=5;
null_ds_cell=cell(1,n_null);
for k=1:n_null
w=k/n_null;
null_ds=ds;
prm=mod(k+(1:6),6)+1;
null_ds.samples=(w*ds.samples(:,prm)+(1-w)*ds.samples(prm,:)')/2;
null_ds_cell{k}=cosmo_slice(null_ds,prm,1,false);
end
opt=struct();
aet(ds,nh1,opt);
opt.h0_mean=0;
aet(ds,nh1,opt);
opt.niter=10;
opt.progress=false;
opt.null=ds;
aet(ds,nh1,opt);
wrong_ds=cosmo_stack({ds,ds});
opt.null=wrong_ds;
aet(ds,nh1,opt);
% different targets
opt.null=null_ds_cell;
opt.null{end}.sa.targets(:)=0;
aet(ds,nh1,opt);
% no dataset
opt.null=null_ds_cell;
opt.null{end}=struct();
aet(ds,nh1,opt);
% sample size mismatch
opt.null=null_ds_cell;
opt.null{end}=cosmo_stack(opt.null([end end]));
aet(ds,nh1,opt);
% .fa mismatch
opt.null=null_ds_cell;
opt.null{end}.fa.i=opt.null{end}.fa.i(end:-1:1);
aet(ds,nh1,opt);
% missing .sa.targets
opt.null=null_ds_cell;
opt.null{end}.sa=rmfield(opt.null{end}.sa,'targets');
aet(ds,nh1,opt);
% .sa.chunks mismatch
opt.null=null_ds_cell;
opt.null{end}.sa.chunks=opt.null{end}.sa.chunks+1;
aet(ds,nh1,opt);
% no feature_sizes
opt.null=null_ds_cell;
nh_bad=nh1;
nh_bad.fa=rmfield(nh_bad.fa,'sizes');
aet(ds,nh_bad,opt);
% check with standard datasets
opt.null=null_ds_cell;
opt.seed=1;
opt.h0_mean=0;
z_ds1=cosmo_montecarlo_cluster_stat(ds,nh1,opt);
assertElementsAlmostEqual(z_ds1.samples,...
[0 -0.90846 0.34876 0 0 0],...
'absolute',1e-4);
opt.h0_mean=0.5;
z_ds2=cosmo_montecarlo_cluster_stat(ds,nh1,opt);
assertElementsAlmostEqual(z_ds2.samples,...
[0 -0.11419 1.3352 0 1.3352 0],...
'absolute',1e-4);
opt=rmfield(opt,'seed');
for k=1:numel(opt.null)
opt.null{k}.samples=opt.null{k}.samples-10;
end
opt.h0_mean=-10;
z_ds3=cosmo_montecarlo_cluster_stat(ds,nh1,opt);
assertElementsAlmostEqual(z_ds3.samples,...
repmat(1.3352,1,6),...
'absolute',1e-4);
function test_feature_stat_montecarlo_cluster_stat
% when using 'feature_stat','none':
% - size(ds.samples,1) must be 1
% - the option 'null' must be provided
% - the option 'niter' must be 0
% - h0_mean is required (but can be zero)
aet=@(varargin)assertExceptionThrown(@()...
cosmo_montecarlo_cluster_stat(varargin{:}),'');
opt=cosmo_structjoin({'niter',10,...
'h0_mean',0,...
'progress',false,...
'seed',1,...
'cluster_stat','tfce',...
'dh',.1});
ds6=cosmo_synthetic_dataset('ntargets',1,'nchunks',6);
nh=cosmo_cluster_neighborhood(ds6,'progress',false);
% using default for 'feature_stat' option gives same output as 'auto'
res1=cosmo_montecarlo_cluster_stat(ds6,nh,opt,...
'feature_stat','auto');
res2=cosmo_montecarlo_cluster_stat(ds6,nh,opt);
assertEqual(res1,res2);
% get dataset with single sample
ds1=cosmo_slice(ds6,1);
% crash with illegal inputs
aet(ds1,nh,opt,'feature_stat','foo');
aet(ds1,nh,opt,'feature_stat',1);
aet(ds1,nh,opt,'feature_stat',false);
% cannot have 'niter' option, even with 'null' option
n_null=10;
null_cell=arrayfun(@(x)cosmo_synthetic_dataset('ntargets',1,...
'nchunks',1',...
'seed',x),...
1:n_null,...
'UniformOutput',false);
opt.feature_stat='none';
aet(ds1,nh,opt,'null',null_cell);
opt=rmfield(opt,'niter');
% cannot be without 'null' option
aet(ds1,opt);
opt.null=null_cell;
% error when using more than one sample
aet(ds6,nh,opt);
% when using default TFCE, dh must be provided
opt_no_dh=rmfield(opt,'dh');
aet(ds1,nh,opt_no_dh);
% simple regression test
res=cosmo_montecarlo_cluster_stat(ds1,nh,opt);
expected_samples=[0 0 0 0 0.11419 0];
assertElementsAlmostEqual(res.samples,expected_samples,...
'absolute',1e-4)
assertEqual(res.fa,ds1.fa);
assertEqual(res.a,ds1.a);
% should also work when no .sa present, or no .sa.targets
ds_no_sa=rmfield(ds1,'sa');
res_no_sa=cosmo_montecarlo_cluster_stat(ds_no_sa,nh,opt);
assertElementsAlmostEqual(res_no_sa.samples,res.samples);
ds_no_targets=ds1;
ds_no_targets.sa=rmfield(ds_no_targets.sa,'targets');
res_no_targets=cosmo_montecarlo_cluster_stat(ds_no_targets,nh,opt);
assertElementsAlmostEqual(res_no_targets.samples,res.samples);
% h0_mean should work
c=8+rand();
null_cell_const=null_cell;
for k=1:numel(null_cell_const)
null_cell_const{k}.samples=null_cell_const{k}.samples+c;
end
ds1_const=ds1;
ds1_const.samples=ds1_const.samples+c;
opt.h0_mean=c;
opt.null=null_cell_const;
res_const=cosmo_montecarlo_cluster_stat(ds1_const,nh,opt);
assertElementsAlmostEqual(res_const.samples,res.samples,...
'absolute',1e-4);
assertEqual(res.fa,res_const.fa);
assertEqual(res.a,res_const.a);
function test_montecarlo_cluster_stat_exceptions
aet=@(varargin)assertExceptionThrown(@()...
cosmo_montecarlo_cluster_stat(varargin{:}),'');
% dh not allowed with non-tfce cluster stat
ds=cosmo_synthetic_dataset();
nh=cosmo_cluster_neighborhood(ds,'progress',false);
opt=struct();
opt.progress=false;
opt.niter=10;
% cannot have dh with non-tfce cluster stat
aet(ds,nh,opt,'cluster_stat','maxsize','p_uncorrected',.01,'dh',.1);
% cannot have p_uncorreced with tfce cluster stat
aet(ds,nh,opt,'p_uncorrected',.01);
aet(ds,nh,opt,'p_uncorrected',.01,'cluster_stat','tfce');
% when using feature_stat none, the null option is required
ds1=cosmo_slice(ds,1);
aet(ds1,nh,'feature_stat','none','dh',.01,'h0_mean',0)
function test_montecarlo_cluster_stat_default_dh
ds=cosmo_synthetic_dataset('size','normal',...
'ntargets',1,'nchunks',15);
ds.samples=randn(size(ds.samples))+.5;
nh=cosmo_cluster_neighborhood(ds,'progress',false);
opt=struct();
opt.progress=false;
opt.h0_mean=0;
% make output detereministic over multiple runs
opt.niter=ceil(rand()*5+15);
opt.seed=ceil(rand()*100+1);
% default value of dh should give identical result
res_no_dh=cosmo_montecarlo_cluster_stat(ds,nh,opt);
res_dh_naught_one=cosmo_montecarlo_cluster_stat(ds,nh,opt,'dh',.1);
assertEqual(res_no_dh,res_dh_naught_one);
% different dh value should (almost always) give different result
res_dh_naught_five=cosmo_montecarlo_cluster_stat(ds,nh,opt,'dh',.5);
assertFalse(isequal(res_no_dh.samples,res_dh_naught_five.samples));
min_corr=.7;
assertTrue(corr(res_dh_naught_one.samples(:),...
res_dh_naught_five.samples(:))>min_corr);
function test_montecarlo_cluster_stat_tiny_niter_singlethread
helper_test_tiny_niter_with_nproc(1);
function test_montecarlo_cluster_stat_tiny_niter_multithread
if cosmo_parallel_get_nproc_available()<=1
cosmo_notify_test_skipped('No parallel toolbox available');
return;
end
helper_test_tiny_niter_with_nproc(2);
function helper_test_tiny_niter_with_nproc(nproc)
ds=cosmo_synthetic_dataset('size','normal',...
'ntargets',1,'nchunks',15);
nh=cosmo_cluster_neighborhood(ds,'progress',false);
opt=struct();
opt.progress=false;
opt.h0_mean=0;
% single iteration
opt.niter=1;
% set nproc
opt.nproc=nproc;
% should not raise an error
cosmo_montecarlo_cluster_stat(ds,nh,opt);