cosmo find local extrema

function [feature_ids,scores]=cosmo_find_local_extrema(ds, nbrhood, varargin)
% find local extrema in a dataset using a neighborhood
%
% [feature_ids,scores]=cosmo_find_local_extrema(ds, nbrhood, ...)
%
% Inputs:
%   ds              dataset struct with one sample, i.e. size(ds.samples,1)
%                   must be 1. NaN values ds.samples are ignored.
%   nbrhood         neighborhood structure corresponding with dataset, with
%                   a field .neighbors so that .neighbors{k} contains the
%                   feature ids of neighbors of feature k.
%   'fitness', f    optional function handle so that [score,id]=f(x)
%                   returns the index with the maximum 'feature score'.
%                   The default is @(x)max(x,[],2), i.e. a function that
%                   returns the maximum value and the index of the maximum
%                   value of the input.
%   'count', c      optional number of elements to select. The default is
%                   Inf.
%
% Output:
%   feature_ids     1xN vector with feature ids.
%   scores          1xN vector with fitness scores. It holds for all I and
%                   J in 1:N that, if I<J, then:
%                   - fitness(ds.samples(I))>fitness(ds.samples(J))
%                   - intersect(nbrhood.neighbors{I},nbrhood.neighbors{J})
%                       is empty
%                   - if I_N=setdiff(nbrhood.neighbors{I},I), then
%                     fitness(ds.samples(I_N))<=fitness(ds.samples(I))
%
%
% Example:
%     % Generate tiny dataset with 6 voxels
%     ds=cosmo_synthetic_dataset('ntargets',1,'nchunks',1);
%     cosmo_disp(ds.samples)
%     %|| [ 2.03     0.584     -1.44    -0.518      1.19     -1.33 ]
%     %
%     % show unflattened shape
%     cosmo_disp(squeeze(cosmo_unflatten(ds)))
%     %|| [  2.03    -0.518
%     %||   0.584      1.19
%     %||   -1.44     -1.33 ]
%     %
%     nh=cosmo_spherical_neighborhood(ds,'radius',1,'progress',false);
%     % find local maxima within neighborhood of 1 voxel radius
%     [feature_ids,scores]=cosmo_find_local_extrema(ds,nh);
%     cosmo_disp(feature_ids)
%     %|| [ 1         5         3 ]
%     cosmo_disp(scores)
%     %|| [ 2.03      1.19     -1.44 ]
%     %
%     % only return two feature ids
%     [feature_ids,scores]=cosmo_find_local_extrema(ds,nh,'count',2);
%     cosmo_disp(feature_ids)
%     %|| [ 1         5 ]
%     cosmo_disp(scores)
%     %|| [ 2.03      1.19 ]
%     %
%     % use another fitness function, namely local minima
%     [feature_ids,scores]=cosmo_find_local_extrema(ds,nh,'fitness',@min);
%     cosmo_disp(feature_ids)
%     %|| [ 3         4 ]
%     cosmo_disp(scores)
%     %|| [ -1.44    -0.518 ]
%     %
%     % find local maxima within neighborhood of 2 voxel radius
%     nh=cosmo_spherical_neighborhood(ds,'radius',2,'progress',false);
%     [feature_ids,scores]=cosmo_find_local_extrema(ds,nh);
%     cosmo_disp(feature_ids)
%     %|| [ 1         6 ]
%     cosmo_disp(scores)
%     %|| [ 2.03     -1.33 ]
%
%
%     % A more sophisticated example showing how a table of local maxima
%     % can be produced:
%     %
%     % Generate synthetic dataset with 700 voxels
%     ds=cosmo_synthetic_dataset('size','big',...
%                                        'ntargets',1,'nchunks',1);
%     %
%     % ensure LPI orientation
%     ds=cosmo_fmri_reorient(ds,'LPI');
%     %
%     % set minimum distance (in voxels) between local extrama (peaks)
%     min_voxels_between_extrema=5;
%     nh=cosmo_spherical_neighborhood(ds,...
%                         'radius',min_voxels_between_extrema,...
%                         'progress',false);
%     %
%     % find local maxima
%     % (note: local minima are not found; this would require a
%     %       'fitness' argument different from the default)
%     [feature_ids,scores]=cosmo_find_local_extrema(ds,nh);
%     %
%     % define and apply threshold
%     thr=1.96;
%     msk=scores>thr;
%     feature_ids=feature_ids(msk);
%     scores=scores(msk);
%     %
%     % get coordinates of local extrama
%     xyz=cosmo_vol_coordinates(ds,feature_ids);
%     %
%     % show table with coordinates and value at local extrema
%     fprintf('x=% 3d y=% 3d z=% 3d   score=%.3f\n',[xyz; scores]);
%     %|| x= 11 y=  7 z=  3   score=3.679
%     %|| x= -1 y=  3 z= -1   score=3.458
%     %|| x= 27 y=  7 z=  7   score=3.361
%     %|| x= 35 y= -1 z=  3   score=3.288
%     %|| x= 23 y=  1 z= -1   score=2.899
%     %|| x= 35 y= 11 z= -1   score=2.268
%
% Notes:
%   - this function can be used to define regions of interest in a
%     reproducible and objective manner.
%   - to ignore particular features, set their value to NaN
%
% #   For CoSMoMVPA's copyright information and license terms,   #
% #   see the COPYING file distributed with CoSMoMVPA.           #

    defaults=struct();
    defaults.count=Inf;
    defaults.fitness=@(x)max(x,[],2);
    opt=cosmo_structjoin(defaults,varargin{:});

    % check sanity of input
    cosmo_check_dataset(ds);
    check_compatibility(ds,nbrhood);

    [nsamples,nfeatures]=size(ds.samples);

    if nsamples~=1
        error('dataset must have exactly 1 sample, found %d', nsamples);
    end

    % keep track of which features were visited
    visited=false(1,nfeatures);

    fitness_func=opt.fitness;

    % go over features iteratively
    counter=0;
    feature_ids=zeros(1,10);
    scores=zeros(1,10);
    while true
        if counter>=opt.count
            break;
        end

        ids_to_consider=find(~visited);

        if isempty(ids_to_consider)
            break;
        end

        [score,i]=fitness_func(ds.samples(:,ids_to_consider));

        if isnan(score)
            break;
        end

        id=ids_to_consider(i);
        around_ids=nbrhood.neighbors{id};

        if any(cosmo_match(feature_ids(1:counter),around_ids))
            % in the unlikely case of an assymetric neighborhood, where
            % a previous id did not have the current id as a neighbor but
            % the current id has a previous id as neighbor, still skip the
            % current feature id
            visited(id)=true;
            continue;
        end

        counter=counter+1;
        if counter>numel(feature_ids)
            % allocate more space
            % this should be done at most log2(nfeatures) times
            feature_ids(2*counter)=0;
            scores(2*counter)=0;
        end

        feature_ids(counter)=id;
        scores(counter)=score;
        around_ids=nbrhood.neighbors{id};
        visited(around_ids)=true;
    end

    feature_ids=feature_ids(1:counter);
    scores=scores(1:counter);


function check_compatibility(ds,nbrhood)
    cosmo_check_neighborhood(nbrhood,ds);