test spherical neighborhood

function test_suite = test_spherical_neighborhood
% tests for cosmo_spherical_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_simple_neighborhood
    ds=cosmo_synthetic_dataset();
    nh1=cosmo_spherical_neighborhood(ds,'radius',0,'progress',false);

    mp=cosmo_align({ds.fa.i, ds.fa.j, ds.fa.k},...
                        {nh1.fa.i, nh1.fa.j, nh1.fa.k});
    ds_al=cosmo_slice(ds,mp,2);
    assertEqual(nh1.a,ds.a);
    assertEqual(nh1.fa.i,ds_al.fa.i);
    assertEqual(nh1.fa.j,ds_al.fa.j);
    assertEqual(nh1.fa.k,ds_al.fa.k);
    assertEqual(nh1.fa.nvoxels,ones(1,6));
    assertEqual(nh1.fa.radius,zeros(1,6));
    assertEqual(nh1.fa.center_ids,mp);
    assertEqual(nh1.neighbors,num2cell(mp'));

    assertEqual(nh1.origin.fa,ds_al.fa);
    assertEqual(nh1.origin.a.fdim,ds_al.a.fdim);


    nh2=cosmo_spherical_neighborhood(ds,'radius',1.5,'progress',false);
    mp=cosmo_align({ds.fa.i, ds.fa.j, ds.fa.k},...
                        {nh1.fa.i, nh1.fa.j, nh1.fa.k});
    ds_al=cosmo_slice(ds,mp,2);
    assertEqual(nh2.a,ds.a);
    assertEqual(nh2.fa.i,ds_al.fa.i);
    assertEqual(nh2.fa.j,ds_al.fa.j);
    assertEqual(nh2.fa.k,ds_al.fa.k);
    nvoxels=[4 6 4 4 6 4];
    assertEqual(nh2.fa.nvoxels,nvoxels(mp));
    assertEqual(nh2.fa.radius,ones(1,6)*1.5);
    assertEqual(nh2.fa.center_ids,mp);
    assertEqual(numel(nh2.neighbors),6);
    nh={ [ 1 4 2 5 ];...
         [ 2 1 5 3 4 6 ];...
         [ 3 2 6 5 ];...
         [ 4 1 5 2 ];...
         [ 5 4 2 6 1 3 ];...
         [ 6 5 3 2 ] };
    for k=1:6
        assertEqual(nh2.neighbors{k},nh{mp(k)});
    end


    nh3=cosmo_spherical_neighborhood(ds,'count',4,'progress',false);
    mp=cosmo_align({ds.fa.i, ds.fa.j, ds.fa.k},...
                        {nh1.fa.i, nh1.fa.j, nh1.fa.k});
    ds_al=cosmo_slice(ds,mp,2);
    assertEqual(nh3.a,ds.a);
    assertEqual(nh3.fa.i,ds_al.fa.i);
    assertEqual(nh3.fa.j,ds_al.fa.j);
    assertEqual(nh3.fa.k,ds_al.fa.k);
    assertEqual(nh3.fa.nvoxels,[4 4 4 4 4 4]);
    radii=[sqrt(2) 1 sqrt(2) sqrt(2) 1 sqrt(2)];
    assertElementsAlmostEqual(nh3.fa.radius,...
                                radii(mp),...
                                'relative',1e-3);
    assertEqual(nh3.fa.center_ids,mp);
    assertEqual(numel(nh3.neighbors),6);
    nh={ [ 1 4 2 5 ];...
         [ 2 1 5 3 ];...
         [ 3 2 6 5 ];...
         [ 4 1 5 2 ];...
         [ 5 4 2 6 ];...
         [ 6 5 3 2 ] };
    for k=1:6
        assertEqual(nh3.neighbors{k},nh{mp(k)});
    end

function test_exceptions
    ds=cosmo_synthetic_dataset();
    aet=@(x)assertExceptionThrown(@()...
                cosmo_spherical_neighborhood(x{:}),'');
    aet({ds});
    aet({ds,'foo'});
    aet({ds,'foo',1});
    aet({ds,'radius',[1 2]});
    aet({ds,'count',[1 2]});
    aet({ds,'radius',-1});
    aet({ds,'count',-1});
    aet({ds,'radius',1,'count',1});
    aet({ds,'count',7});
    aet({'foo','count',7});

function test_sparse_dataset
    nfeatures_test=3;

    ds=cosmo_synthetic_dataset('size','big');
    nf=size(ds.samples,2);
    rp=randperm(nf);
    nf_full=round(nf*.4);
    ids=rp(1:nf_full);

    ds_sel=cosmo_slice(ds,ids,2);

    ids_full=repmat(ids,1,2);
    ds_full=cosmo_slice(ds,ids_full,2);

    radius=2+rand();
    nh4=cosmo_spherical_neighborhood(ds_full,'radius',radius,...
                                    'progress',false);
    assertEqual(numel(nh4.neighbors),numel(ids));
    mp=cosmo_align({ds_sel.fa.i, ds_sel.fa.j, ds_sel.fa.k},...
                        {nh4.fa.i, nh4.fa.j, nh4.fa.k});
    ds_al=cosmo_slice(ds_sel,mp,2);

    assertEqual(nh4.a,ds.a);
    assertEqual(nh4.fa.i,ds_al.fa.i);
    assertEqual(nh4.fa.j,ds_al.fa.j);
    assertEqual(nh4.fa.k,ds_al.fa.k);
    assertEqual(nh4.fa.center_ids,mp);

    rp=randperm(size(ds_al.samples,2));
    center_ids=rp(1:nfeatures_test);

    ijk=[ds_al.fa.i; ds_al.fa.j; ds_al.fa.k];
    ijk_full=[ds_full.fa.i; ds_full.fa.j; ds_full.fa.k];
    for center_id=center_ids
        ijk_center=ijk(:,center_id);
        delta=sum(bsxfun(@minus,ijk_center,ijk_full).^2,1).^.5;
        nbr_ids=find(delta<=radius);
        assertEqual(nbr_ids,sort(nh4.neighbors{center_id}));
    end


function test_with_freq_dimension_dataset
    ds=cosmo_synthetic_dataset('size','big');
    nfeatures=size(ds.samples,2);

    freqs=[2 4 6];
    nfreqs=numel(freqs);
    ds_cell=cell(nfreqs,1);
    for k=1:nfreqs
        ds_freq=ds;
        ds_freq.a.fdim.labels=[{'freq'};ds_freq.a.fdim.labels];
        ds_freq.a.fdim.values=[{freqs};ds_freq.a.fdim.values];
        ds_freq.fa.freq=ones(1,nfeatures)*k;
        ds_cell{k}=ds_freq;
    end

    ds_full=cosmo_stack(ds_cell,2);
    rp=randperm(nfeatures*nfreqs);
    id_full=rp(1:((nfreqs-1)*nfeatures));
    ds_full=cosmo_slice(ds_full,id_full,2);

    ijk_full=[ds_full.fa.i; ds_full.fa.j; ds_full.fa.k];
    [ijk_idxs,ijk_unq]=cosmo_index_unique(ijk_full');
    ijk_idx=cellfun(@(x)x(1),ijk_idxs);
    ds_al=cosmo_slice(ds_full,ijk_idx,2);

    radius=5+rand()*3;
    nh=cosmo_spherical_neighborhood(ds_full,'radius',radius,...
                                            'progress',false);

    assertEqual(nh.fa.i,ds_al.fa.i);
    assertEqual(nh.fa.j,ds_al.fa.j);
    assertEqual(nh.fa.k,ds_al.fa.k);
    assertEqual(nh.a.fdim.labels,ds_full.a.fdim.labels(2:4));
    assertEqual(nh.a.fdim.values,ds_full.a.fdim.values(2:4));
    assertEqual(numel(nh.neighbors),numel(ijk_idxs));

    ijk_full=[ds_full.fa.i; ds_full.fa.j; ds_full.fa.k];
    ijk_al=[ds_al.fa.i; ds_al.fa.j; ds_al.fa.k];

    % test match for euclidean distance
    rp=randperm(numel(ijk_idxs));
    rp=rp(1:10);
    for r=rp
        nbrs=nh.neighbors{r};

        ijk_center=ijk_al(:,r);
        delta=sum(bsxfun(@minus,ijk_center,ijk_full).^2,1).^.5;
        assertEqual(find(delta<=radius), sort(nbrs));
    end

    % test with transposed dimension values
    ds2_full=ds_full;
    ds2_full.a.fdim.values=cellfun(@transpose,...
                            ds2_full.a.fdim.values,...
                            'UniformOutput',false)';
    nh2=cosmo_spherical_neighborhood(ds2_full,'radius',radius,...
                                                'progress',false);

    nh2.origin.a.fdim.values=cellfun(@transpose,...
                             nh2.origin.a.fdim.values,...
                            'UniformOutput',false)';
    assertEqual(nh,nh2);
    assertFalse(isfield(nh.fa,'inside'));

function test_meeg_source_dataset
    ds=cosmo_synthetic_dataset('type','source','size','normal');
    nf=size(ds.samples,2);

    [unused,idxs]=sort(cosmo_rand(1,nf*4,'seed',1));
    rps=mod(idxs-1,nf)+1;
    id_full=rps(round(nf/2)+(1:(3*nf)));
    ds_full=cosmo_slice(ds,id_full,2);

    pos_full=ds_full.fa.pos;
    pos_idxs=cosmo_index_unique(pos_full');
    pos_idx=cellfun(@(x)x(1),pos_idxs);
    ds_unq=cosmo_slice(ds_full,pos_idx,2);

    radius=1.2+.2*rand();

    nh=cosmo_spherical_neighborhood(ds_full,'radius',radius,...
                                                'progress',false);
    mp=cosmo_align(ds_unq.fa.pos',nh.fa.pos');
    ds_al=cosmo_slice(ds_unq,mp,2);
    assertEqual(nh.fa.pos,ds_al.fa.pos);
    assertEqual(nh.a.fdim.labels{1},ds.a.fdim.labels{1});
    assertEqual(nh.a.fdim.values{1},ds.a.fdim.values{1});
    assertEqual(numel(nh.neighbors),numel(pos_idx));

    count=ceil(4/3*pi*(radius)^3 * .5);
    nh2=cosmo_spherical_neighborhood(ds_full,'count',count,...
                                                'progress',false);
    assertEqual(nh2.fa.pos,ds_al.fa.pos);
    assertEqual(nh2.a.fdim.labels{1},ds.a.fdim.labels{1});
    assertEqual(nh2.a.fdim.values{1},ds.a.fdim.values{1});
    assertEqual(numel(nh2.neighbors),numel(pos_idx));

    pos_al=ds_al.a.fdim.values{1}(:,ds_al.fa.pos);
    pos_full=nh.a.fdim.values{1}(:,ds_full.fa.pos);

    voxel_size=10;
    for r=1:numel(pos_idx)
        d=sum(bsxfun(@minus,pos_full,pos_al(:,r)).^2,1).^.5;
        idxs=find(d<=(radius*voxel_size));

        assertEqual(sort(nh.neighbors{r}),sort(idxs));
    end

    [p,q]=cosmo_overlap(nh.neighbors,nh2.neighbors);

    dp=diag(p);
    dq=diag(q);

    assertTrue(mean(dp)>.8);
    assertTrue(mean(dq)>.3);

function test_small_meeg_source_dataset_without_inside_field
    ds=cosmo_synthetic_dataset('type','source','size','small');
    voxel_size=10;
    idx=cosmo_index_unique(ds.fa.pos');
    n_pos=numel(idx);

    ds_pos=ds.a.fdim.values{1}(:,ds.fa.pos);

    for radius=0:.4:2;
        nh=cosmo_spherical_neighborhood(ds,...
                    'radius',radius,...
                    'progress',false);
        center_pos=nh.a.fdim.values{1}(:,nh.fa.pos);
        assertEqual(numel(nh.neighbors),n_pos);
        for k=1:n_pos
            sel_idx=nh.neighbors{k};

            delta=bsxfun(@minus,center_pos(:,k),...
                                 ds_pos);
            distance=sqrt(sum((delta/voxel_size).^2,1));

            expected_sel_idx=find(distance<=radius);

            assertEqual(sort(sel_idx(:)),sort(expected_sel_idx(:)));
        end
    end



function test_small_meeg_source_dataset_with_inside_field
    ds=cosmo_synthetic_dataset('type','source','size','small');
    voxel_size=10;
    nf=size(ds.samples,2);

    idx=cosmo_index_unique(ds.fa.pos');
    n_pos=numel(idx);

    for keep_ratio=.4:.3:1;
        keep=randperm(n_pos);
        n_keep=round(n_pos*keep_ratio);
        keep=keep(1:n_keep);

        ds.fa.inside=false(1,nf);
        for k=1:numel(keep)
            ds.fa.inside(idx{keep(k)})=true;
        end

        ds_pos=ds.a.fdim.values{1}(:,ds.fa.pos);

        for radius=0:.4:2;
            nh=cosmo_spherical_neighborhood(ds,...
                        'radius',radius,...
                        'progress',false);
            center_pos=nh.a.fdim.values{1}(:,nh.fa.pos);
            assertEqual(numel(nh.neighbors),n_keep);
            for k=1:n_keep
                sel_idx=nh.neighbors{k};

                delta=bsxfun(@minus,center_pos(:,k),...
                                     ds_pos);
                distance=sqrt(sum((delta/voxel_size).^2,1));

                expected_sel_idx=find(distance<=radius & ds.fa.inside);

                assertEqual(sort(sel_idx(:)),sort(expected_sel_idx(:)));
            end
        end
    end

function test_meeg_source_illegal_inside()
    ds=cosmo_synthetic_dataset('type','source','size','small');
    n_features=size(ds.samples,1);

    illegal_values={min(max(ds.samples(1,:),0),1),...
                    repmat({'foo'},1,n_features)};
    for k=1:numel(illegal_values)
        ds.fa.inside=illegal_values{k};

        assertExceptionThrown(@()...
                cosmo_spherical_neighborhood(ds,'radius',1),'');
    end

function test_meeg_missing_dimension_label()
    ds=cosmo_synthetic_dataset();
    ds=cosmo_dim_remove(ds,'i');
    assertExceptionThrown(@()...
                cosmo_spherical_neighborhood(ds,'radius',1),'');

function test_meeg_wrong_dimension_order()
    ds=cosmo_synthetic_dataset();
    ds.fa.j(:)=1;
    ds.fa.k(:)=1;
    ds.a.fdim.labels([2,3])=ds.a.fdim.labels([3,2]);
    assertExceptionThrown(@()...
                cosmo_spherical_neighborhood(ds,'radius',1),'');

function test_meeg_source_sdim_time
    ds=cosmo_synthetic_dataset('type','source','size','big');
    ds_time=cosmo_dim_transpose(ds,'time',1);
    nh=cosmo_spherical_neighborhood(ds_time,'radius',0,'progress',false);

    assert(~isfield(nh.a,'sdim'));
    assert(~isfield(nh,'sa'));



function test_fmri_fixed_number_of_features()
    ds=cosmo_synthetic_dataset('size','normal');
    nf=size(ds.samples,2);
    [unused,idxs]=sort(cosmo_rand(1,nf*3,'seed',1));
    rps=mod(idxs-1,nf)+1;
    id_full=rps(round(nf/2)+(1:(2*nf)));
    ds_full=cosmo_slice(ds,id_full,2);

    id_idxs=cosmo_index_unique(id_full');
    id_idx=cellfun(@(x)x(1),id_idxs);
    ds_sel=cosmo_slice(ds_full,id_idx,2);

    count=20;
    nh=cosmo_spherical_neighborhood(ds_full,'count',count,...
                                        'progress',false);
    ijk_sel={ds_sel.fa.i; ds_sel.fa.j; ds_sel.fa.k};
    ijk_nh={nh.fa.i; nh.fa.j; nh.fa.k};

    mp=cosmo_align(ijk_sel,ijk_nh);
    ds_al=cosmo_slice(ds_sel,mp,2);
    assertEqual(nh.fa.i,ds_al.fa.i);
    assertEqual(nh.fa.j,ds_al.fa.j);
    assertEqual(nh.fa.k,ds_al.fa.k);

    pos_nh=[nh.fa.i; nh.fa.j; nh.fa.k];
    pos_full=[ds_full.fa.i;ds_full.fa.j;ds_full.fa.k];

    rp=randperm(numel(id_idx));
    rp=rp(1:10);
    for r=rp
        d=sum(bsxfun(@minus,pos_nh(:,r),pos_full).^2,1).^.5;
        idxs=nh.neighbors{r};
        d_inside=d(idxs);
        d_outside=d(setdiff(1:nf,idxs));
        assert(max(d_inside)<=min(d_outside));
        assertElementsAlmostEqual(max(d_inside),nh.fa.radius(r),...
                                                'absolute',1e-4);
    end