test external surfing

function test_suite = test_external_surfing()
    % regression tests for external "surfing" toolbox
    %
    % #   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_surfing_subsample_surface()
    if cosmo_skip_test_if_no_external('surfing')
        return
    end
    opt = struct();
    opt.progress = false;

    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]';

    [v_sub, f_sub] = surfing_subsample_surface(vertices, faces, 1, .2, false);

    assertEqual(v_sub', [-1 -2 -1 1 2 1 3 4 3
                         -2 0 2 2 0 -2 2 0 -2
                         0 0 0 0 0 0 0 0 0]);
    assertEqual(f_sub', [1 1 1 1 4 5 7 5
                         2 3 4 5 7 9 8 8
                         3 4 5 6 5 6 5 9]);

    [v_sub2, f_sub2] = surfing_subsample_surface(v_sub, f_sub, 1, .2, false);
    assertEqual(v_sub2', [-1 -2 -1 1 1 3 4 3
                          -2 0 2 2 -2 2 0 -2
                          0 0 0 0 0 0 0 0]);

    assertEqual(f_sub2', [1 1 1 4 6 4
                          2 3 4 8 7 7
                          3 4 5 5 4 8]);

    [v_sub2_alt, f_sub2_alt] = surfing_subsample_surface(vertices, faces, ...
                                                         2, .2, false);
    assertEqual(v_sub2_alt, v_sub2);
    assertEqual(f_sub2_alt, f_sub2);

function test_surfing_generate_planar_surface()
    if cosmo_skip_test_if_no_external('surfing')
        return
    end

    if isempty(which('surfing_generate_planar_surface'))
        % older versions of surfing
        cosmo_notify_test_skipped(['surfing_generate_planar_surface '...
                                   'is not available']);
        return
    end

    nx = round(rand() * 5) + 5;
    ny = round(rand() * 5) + 5;

    origin = rand(3, 1) * 10;
    x1 = rand(3, 1) * 10;
    y1 = rand(3, 1) * 10;

    [v1, f1] = generate_planar_surface(nx, ny, origin, x1, y1);
    [v2, f2] = surfing_generate_planar_surface(nx, ny, origin, x1, y1);
    assertElementsAlmostEqual(v1, v2);
    assertElementsAlmostEqual(f1, f2);

    % test default options
    [v1, f1] = generate_planar_surface(nx, ny, [0, 0, 0], [1, 0, 0], [0, 1, 0]);
    [v2, f2] = surfing_generate_planar_surface(nx, ny);
    assertElementsAlmostEqual(v1, v2);
    assertElementsAlmostEqual(f1, f2);

function test_surfing_voxel_selection_dijkstra()
    helper_test_surfing_voxel_selection('dijkstra');

function test_surfing_voxel_selection_euclidean()
    helper_test_surfing_voxel_selection('euclidean');

function helper_test_surfing_voxel_selection(metric)
    if cosmo_skip_test_if_no_external('surfing')
        return
    end
    warning_state = warning();
    warning_resetter = onCleanup(@()warning(warning_state));
    warning('off', 'all');

    nx = round(rand() * 5) + 5;
    ny = round(rand() * 5) + 5;

    % surface with some nodes outside the volume
    [v, f] = generate_planar_surface(nx, ny, [-4, -4, 0], [1, 0, 0], [0, 1, 0]);
    thickness = rand() * 2 + 2;
    pial = bsxfun(@plus, v, [0, 0, -thickness])';
    white = bsxfun(@plus, v, [0, 0, thickness])';

    ds = cosmo_synthetic_dataset('size', 'big');
    ds_xyz = cosmo_vol_coordinates(ds);
    voldef = ds.a.vol;
    half_vox_size = voldef.mat(1);
    thickness_margin = thickness + half_vox_size;

    radius = rand() * 4 + 2;
    args = {pial, white, f', radius, voldef, [], [], metric, 0};
    n2v = surfing_voxelselection(args{:});

    switch metric
        case 'dijkstra'
            extra_distance = 0;

        case 'euclidean'
            extra_distance = 0;

        otherwise
            error('illegal distance ''%s''', metric);
    end

    nv = size(v, 1);
    rp = randperm(nv);

    for k = 1:nv / 2
        node_id = rp(k);
        node_xyz = v(node_id, :);

        is_inside = ~isequal(cosmo_vol_coordinates(ds, node_xyz'), 0);
        voxel_ids = n2v{node_id};
        has_voxels = ~isempty(voxel_ids);

        assertEqual(is_inside, has_voxels);
        if is_inside
            voxels_xyz = ds_xyz(:, n2v{node_id})';
            voxels_z = voxels_xyz(:, 3);

            % check z coordinates
            assert(all(-thickness_margin <= voxels_z & ...
                       voxels_z <= thickness_margin));

            distances = euclidean_distance(ds_xyz', node_xyz, 1:3);

            node_distances = distances(n2v{node_id});
            assert(all(node_distances <= radius + thickness_margin));

            % check voxels not selected
            nfeatures = size(ds_xyz, 2);
            candidates = true(nfeatures, 1);
            candidates(ds_xyz(1, :) <= min(v(:, 1))) = false;
            candidates(ds_xyz(2, :) <= min(v(:, 2))) = false;
            candidates(ds_xyz(3, :) <= -(thickness + half_vox_size)) = false;
            candidates(ds_xyz(1, :) >= max(v(:, 1))) = false;
            candidates(ds_xyz(2, :) >= max(v(:, 2))) = false;
            candidates(ds_xyz(3, :) >= thickness + half_vox_size) = false;
            candidates(n2v{node_id}) = false;

            if any(candidates)
                min_outside = min(distances(candidates));
                assert(min_outside > radius - half_vox_size);
            end
        end

        % check individual call for this node id
        args_single_node = args;
        args_single_node{6} = node_id;
        n2v_single_node = surfing_voxelselection(args_single_node{:});
        assertEqual(n2v_single_node, n2v(node_id));
    end

function d = euclidean_distance(p, q, dim)
    d = sqrt(sum(bsxfun(@minus, p(:, dim), q(:, dim)).^2, 2));

function [vertices, faces] = generate_planar_surface(nx, ny, origin, x1, y1)

    vertices = zeros(nx * ny, 3);
    faces = zeros(2 * (nx - 1) * (ny - 1), 3);

    for i = 1:nx
        for j = 1:ny
            vpos = (i - 1) * ny + j;
            vertices(vpos, :) = origin + (i - 1) * x1 + (j - 1) * y1;
            if i < nx && j < ny
                p = vpos;
                q = vpos + 1;
                r = vpos + ny;
                s = vpos + ny + 1;

                fpos = ((i - 1) * (ny - 1) + j) * 2 - 1;
                faces(fpos, :) = [p, q, r];
                faces(fpos + 1, :) = [s, r, q];
            end
        end
    end