Coverage Report

Created: 2026-02-27 06:50

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/igraph/src/spatial/nearest_neighbor.cpp
Line
Count
Source
1
/*
2
   igraph library.
3
   Copyright (C) 2025  The igraph development team <igraph@igraph.org>
4
5
   This program is free software; you can redistribute it and/or modify
6
   it under the terms of the GNU General Public License as published by
7
   the Free Software Foundation; either version 2 of the License, or
8
   (at your option) any later version.
9
10
   This program is distributed in the hope that it will be useful,
11
   but WITHOUT ANY WARRANTY; without even the implied warranty of
12
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
   GNU General Public License for more details.
14
15
   You should have received a copy of the GNU General Public License
16
   along with this program.  If not, see <https://www.gnu.org/licenses/>.
17
*/
18
19
#include "igraph_spatial.h"
20
21
#include "igraph_constructors.h"
22
#include "igraph_conversion.h"
23
#include "igraph_error.h"
24
#include "igraph_interface.h"
25
#include "igraph_matrix.h"
26
#include "igraph_types.h"
27
#include "igraph_vector.h"
28
29
#include "spatial/nanoflann_internal.hpp"
30
31
#include "core/exceptions.h"
32
#include "core/interruption.h"
33
#include "spatial/spatial_internal.h"
34
35
#include "nanoflann/nanoflann.hpp"
36
37
#include <vector>
38
39
template <typename Metric, igraph_int_t Dimension>
40
static igraph_error_t neighbor_helper(
41
        igraph_t *graph,
42
        const igraph_matrix_t *points,
43
        igraph_int_t k,
44
        igraph_real_t cutoff,
45
        igraph_int_t dimension,
46
3.62k
        igraph_bool_t directed) {
47
48
3.62k
    const igraph_int_t point_count = igraph_matrix_nrow(points);
49
3.62k
    ig_point_adaptor adaptor(points);
50
3.62k
    int iter = 0;
51
52
3.62k
    using kdTree = nanoflann::KDTreeSingleIndexAdaptor<Metric, ig_point_adaptor, Dimension, igraph_int_t>;
53
3.62k
    kdTree tree(dimension, adaptor, nanoflann::KDTreeSingleIndexAdaptorParams(10));
54
55
3.62k
    tree.buildIndex();
56
57
3.62k
    igraph_vector_t current_point;
58
3.62k
    IGRAPH_VECTOR_INIT_FINALLY(&current_point, dimension);
59
60
3.62k
    igraph_int_t neighbor_count = k >= 0 ? k : point_count;
61
62
3.62k
    GraphBuildingResultSet results(neighbor_count, cutoff);
63
3.62k
    std::vector<igraph_int_t> edges;
64
79.2k
    for (igraph_int_t i = 0; i < point_count; i++) {
65
75.6k
        results.reset(i);
66
75.6k
        IGRAPH_CHECK(igraph_matrix_get_row(points, &current_point, i));
67
68
75.6k
        tree.findNeighbors(results, VECTOR(current_point), nanoflann::SearchParameters(0, false));
69
174k
        for (igraph_int_t j = 0; j < results.size(); j++) {
70
99.2k
            edges.push_back(i);
71
99.2k
            edges.push_back(results.neighbors[j]);
72
99.2k
        }
73
74
75.6k
        IGRAPH_ALLOW_INTERRUPTION_LIMITED(iter, 1 << 10);
75
75.6k
    }
76
77
3.62k
    igraph_vector_destroy(&current_point);
78
3.62k
    IGRAPH_FINALLY_CLEAN(1);
79
80
    // Overflow check, ensures that edges.size() is not too large for igraph vectors.
81
3.62k
    if (edges.size() > (size_t) IGRAPH_INTEGER_MAX) {
82
0
        IGRAPH_ERROR("Too many edges.", IGRAPH_EOVERFLOW);
83
0
    }
84
85
3.62k
    const igraph_vector_int_t edge_view = igraph_vector_int_view(edges.data(), edges.size());
86
3.62k
    IGRAPH_CHECK(igraph_create(graph, &edge_view, point_count, true));
87
88
3.62k
    if (! directed) {
89
1.81k
        IGRAPH_CHECK(igraph_to_undirected(graph, IGRAPH_TO_UNDIRECTED_COLLAPSE, NULL));
90
1.81k
    }
91
92
3.62k
    return IGRAPH_SUCCESS;
93
3.62k
}
nearest_neighbor.cpp:igraph_error_type_t neighbor_helper<nanoflann::L2_Adaptor<double, ig_point_adaptor, double, unsigned long>, 1l>(igraph_t*, igraph_matrix_t const*, long, double, long, bool)
Line
Count
Source
46
902
        igraph_bool_t directed) {
47
48
902
    const igraph_int_t point_count = igraph_matrix_nrow(points);
49
902
    ig_point_adaptor adaptor(points);
50
902
    int iter = 0;
51
52
902
    using kdTree = nanoflann::KDTreeSingleIndexAdaptor<Metric, ig_point_adaptor, Dimension, igraph_int_t>;
53
902
    kdTree tree(dimension, adaptor, nanoflann::KDTreeSingleIndexAdaptorParams(10));
54
55
902
    tree.buildIndex();
56
57
902
    igraph_vector_t current_point;
58
902
    IGRAPH_VECTOR_INIT_FINALLY(&current_point, dimension);
59
60
902
    igraph_int_t neighbor_count = k >= 0 ? k : point_count;
61
62
902
    GraphBuildingResultSet results(neighbor_count, cutoff);
63
902
    std::vector<igraph_int_t> edges;
64
37.3k
    for (igraph_int_t i = 0; i < point_count; i++) {
65
36.4k
        results.reset(i);
66
36.4k
        IGRAPH_CHECK(igraph_matrix_get_row(points, &current_point, i));
67
68
36.4k
        tree.findNeighbors(results, VECTOR(current_point), nanoflann::SearchParameters(0, false));
69
94.0k
        for (igraph_int_t j = 0; j < results.size(); j++) {
70
57.5k
            edges.push_back(i);
71
57.5k
            edges.push_back(results.neighbors[j]);
72
57.5k
        }
73
74
36.4k
        IGRAPH_ALLOW_INTERRUPTION_LIMITED(iter, 1 << 10);
75
36.4k
    }
76
77
902
    igraph_vector_destroy(&current_point);
78
902
    IGRAPH_FINALLY_CLEAN(1);
79
80
    // Overflow check, ensures that edges.size() is not too large for igraph vectors.
81
902
    if (edges.size() > (size_t) IGRAPH_INTEGER_MAX) {
82
0
        IGRAPH_ERROR("Too many edges.", IGRAPH_EOVERFLOW);
83
0
    }
84
85
902
    const igraph_vector_int_t edge_view = igraph_vector_int_view(edges.data(), edges.size());
86
902
    IGRAPH_CHECK(igraph_create(graph, &edge_view, point_count, true));
87
88
902
    if (! directed) {
89
902
        IGRAPH_CHECK(igraph_to_undirected(graph, IGRAPH_TO_UNDIRECTED_COLLAPSE, NULL));
90
902
    }
91
92
902
    return IGRAPH_SUCCESS;
93
902
}
nearest_neighbor.cpp:igraph_error_type_t neighbor_helper<nanoflann::L2_Adaptor<double, ig_point_adaptor, double, unsigned long>, 2l>(igraph_t*, igraph_matrix_t const*, long, double, long, bool)
Line
Count
Source
46
905
        igraph_bool_t directed) {
47
48
905
    const igraph_int_t point_count = igraph_matrix_nrow(points);
49
905
    ig_point_adaptor adaptor(points);
50
905
    int iter = 0;
51
52
905
    using kdTree = nanoflann::KDTreeSingleIndexAdaptor<Metric, ig_point_adaptor, Dimension, igraph_int_t>;
53
905
    kdTree tree(dimension, adaptor, nanoflann::KDTreeSingleIndexAdaptorParams(10));
54
55
905
    tree.buildIndex();
56
57
905
    igraph_vector_t current_point;
58
905
    IGRAPH_VECTOR_INIT_FINALLY(&current_point, dimension);
59
60
905
    igraph_int_t neighbor_count = k >= 0 ? k : point_count;
61
62
905
    GraphBuildingResultSet results(neighbor_count, cutoff);
63
905
    std::vector<igraph_int_t> edges;
64
19.0k
    for (igraph_int_t i = 0; i < point_count; i++) {
65
18.1k
        results.reset(i);
66
18.1k
        IGRAPH_CHECK(igraph_matrix_get_row(points, &current_point, i));
67
68
18.1k
        tree.findNeighbors(results, VECTOR(current_point), nanoflann::SearchParameters(0, false));
69
30.1k
        for (igraph_int_t j = 0; j < results.size(); j++) {
70
12.0k
            edges.push_back(i);
71
12.0k
            edges.push_back(results.neighbors[j]);
72
12.0k
        }
73
74
18.1k
        IGRAPH_ALLOW_INTERRUPTION_LIMITED(iter, 1 << 10);
75
18.1k
    }
76
77
905
    igraph_vector_destroy(&current_point);
78
905
    IGRAPH_FINALLY_CLEAN(1);
79
80
    // Overflow check, ensures that edges.size() is not too large for igraph vectors.
81
905
    if (edges.size() > (size_t) IGRAPH_INTEGER_MAX) {
82
0
        IGRAPH_ERROR("Too many edges.", IGRAPH_EOVERFLOW);
83
0
    }
84
85
905
    const igraph_vector_int_t edge_view = igraph_vector_int_view(edges.data(), edges.size());
86
905
    IGRAPH_CHECK(igraph_create(graph, &edge_view, point_count, true));
87
88
905
    if (! directed) {
89
0
        IGRAPH_CHECK(igraph_to_undirected(graph, IGRAPH_TO_UNDIRECTED_COLLAPSE, NULL));
90
0
    }
91
92
905
    return IGRAPH_SUCCESS;
93
905
}
nearest_neighbor.cpp:igraph_error_type_t neighbor_helper<nanoflann::L2_Adaptor<double, ig_point_adaptor, double, unsigned long>, 3l>(igraph_t*, igraph_matrix_t const*, long, double, long, bool)
Line
Count
Source
46
914
        igraph_bool_t directed) {
47
48
914
    const igraph_int_t point_count = igraph_matrix_nrow(points);
49
914
    ig_point_adaptor adaptor(points);
50
914
    int iter = 0;
51
52
914
    using kdTree = nanoflann::KDTreeSingleIndexAdaptor<Metric, ig_point_adaptor, Dimension, igraph_int_t>;
53
914
    kdTree tree(dimension, adaptor, nanoflann::KDTreeSingleIndexAdaptorParams(10));
54
55
914
    tree.buildIndex();
56
57
914
    igraph_vector_t current_point;
58
914
    IGRAPH_VECTOR_INIT_FINALLY(&current_point, dimension);
59
60
914
    igraph_int_t neighbor_count = k >= 0 ? k : point_count;
61
62
914
    GraphBuildingResultSet results(neighbor_count, cutoff);
63
914
    std::vector<igraph_int_t> edges;
64
12.9k
    for (igraph_int_t i = 0; i < point_count; i++) {
65
12.0k
        results.reset(i);
66
12.0k
        IGRAPH_CHECK(igraph_matrix_get_row(points, &current_point, i));
67
68
12.0k
        tree.findNeighbors(results, VECTOR(current_point), nanoflann::SearchParameters(0, false));
69
32.4k
        for (igraph_int_t j = 0; j < results.size(); j++) {
70
20.3k
            edges.push_back(i);
71
20.3k
            edges.push_back(results.neighbors[j]);
72
20.3k
        }
73
74
12.0k
        IGRAPH_ALLOW_INTERRUPTION_LIMITED(iter, 1 << 10);
75
12.0k
    }
76
77
914
    igraph_vector_destroy(&current_point);
78
914
    IGRAPH_FINALLY_CLEAN(1);
79
80
    // Overflow check, ensures that edges.size() is not too large for igraph vectors.
81
914
    if (edges.size() > (size_t) IGRAPH_INTEGER_MAX) {
82
0
        IGRAPH_ERROR("Too many edges.", IGRAPH_EOVERFLOW);
83
0
    }
84
85
914
    const igraph_vector_int_t edge_view = igraph_vector_int_view(edges.data(), edges.size());
86
914
    IGRAPH_CHECK(igraph_create(graph, &edge_view, point_count, true));
87
88
914
    if (! directed) {
89
914
        IGRAPH_CHECK(igraph_to_undirected(graph, IGRAPH_TO_UNDIRECTED_COLLAPSE, NULL));
90
914
    }
91
92
914
    return IGRAPH_SUCCESS;
93
914
}
nearest_neighbor.cpp:igraph_error_type_t neighbor_helper<nanoflann::L2_Adaptor<double, ig_point_adaptor, double, unsigned long>, -1l>(igraph_t*, igraph_matrix_t const*, long, double, long, bool)
Line
Count
Source
46
903
        igraph_bool_t directed) {
47
48
903
    const igraph_int_t point_count = igraph_matrix_nrow(points);
49
903
    ig_point_adaptor adaptor(points);
50
903
    int iter = 0;
51
52
903
    using kdTree = nanoflann::KDTreeSingleIndexAdaptor<Metric, ig_point_adaptor, Dimension, igraph_int_t>;
53
903
    kdTree tree(dimension, adaptor, nanoflann::KDTreeSingleIndexAdaptorParams(10));
54
55
903
    tree.buildIndex();
56
57
903
    igraph_vector_t current_point;
58
903
    IGRAPH_VECTOR_INIT_FINALLY(&current_point, dimension);
59
60
903
    igraph_int_t neighbor_count = k >= 0 ? k : point_count;
61
62
903
    GraphBuildingResultSet results(neighbor_count, cutoff);
63
903
    std::vector<igraph_int_t> edges;
64
9.84k
    for (igraph_int_t i = 0; i < point_count; i++) {
65
8.94k
        results.reset(i);
66
8.94k
        IGRAPH_CHECK(igraph_matrix_get_row(points, &current_point, i));
67
68
8.94k
        tree.findNeighbors(results, VECTOR(current_point), nanoflann::SearchParameters(0, false));
69
18.1k
        for (igraph_int_t j = 0; j < results.size(); j++) {
70
9.23k
            edges.push_back(i);
71
9.23k
            edges.push_back(results.neighbors[j]);
72
9.23k
        }
73
74
8.94k
        IGRAPH_ALLOW_INTERRUPTION_LIMITED(iter, 1 << 10);
75
8.94k
    }
76
77
903
    igraph_vector_destroy(&current_point);
78
903
    IGRAPH_FINALLY_CLEAN(1);
79
80
    // Overflow check, ensures that edges.size() is not too large for igraph vectors.
81
903
    if (edges.size() > (size_t) IGRAPH_INTEGER_MAX) {
82
0
        IGRAPH_ERROR("Too many edges.", IGRAPH_EOVERFLOW);
83
0
    }
84
85
903
    const igraph_vector_int_t edge_view = igraph_vector_int_view(edges.data(), edges.size());
86
903
    IGRAPH_CHECK(igraph_create(graph, &edge_view, point_count, true));
87
88
903
    if (! directed) {
89
0
        IGRAPH_CHECK(igraph_to_undirected(graph, IGRAPH_TO_UNDIRECTED_COLLAPSE, NULL));
90
0
    }
91
92
903
    return IGRAPH_SUCCESS;
93
903
}
Unexecuted instantiation: nearest_neighbor.cpp:igraph_error_type_t neighbor_helper<nanoflann::L1_Adaptor<double, ig_point_adaptor, double, unsigned long>, 1l>(igraph_t*, igraph_matrix_t const*, long, double, long, bool)
Unexecuted instantiation: nearest_neighbor.cpp:igraph_error_type_t neighbor_helper<nanoflann::L1_Adaptor<double, ig_point_adaptor, double, unsigned long>, 2l>(igraph_t*, igraph_matrix_t const*, long, double, long, bool)
Unexecuted instantiation: nearest_neighbor.cpp:igraph_error_type_t neighbor_helper<nanoflann::L1_Adaptor<double, ig_point_adaptor, double, unsigned long>, 3l>(igraph_t*, igraph_matrix_t const*, long, double, long, bool)
Unexecuted instantiation: nearest_neighbor.cpp:igraph_error_type_t neighbor_helper<nanoflann::L1_Adaptor<double, ig_point_adaptor, double, unsigned long>, -1l>(igraph_t*, igraph_matrix_t const*, long, double, long, bool)
94
95
96
template <typename Metric>
97
static igraph_error_t dimension_dispatcher(
98
        igraph_t *graph,
99
        const igraph_matrix_t *points,
100
        igraph_int_t k,
101
        igraph_real_t cutoff,
102
        igraph_int_t dimension,
103
3.62k
        igraph_bool_t directed) {
104
105
3.62k
    switch (dimension) {
106
0
    case 0:
107
0
        IGRAPH_ERROR("0-dimensional points are not supported.", IGRAPH_EINVAL);
108
902
    case 1:
109
902
        return neighbor_helper<Metric, 1>(graph, points, k, cutoff, dimension, directed);
110
905
    case 2:
111
905
        return neighbor_helper<Metric, 2>(graph, points, k, cutoff, dimension, directed);
112
914
    case 3:
113
914
        return neighbor_helper<Metric, 3>(graph, points, k, cutoff, dimension, directed);
114
903
    default:
115
903
        return neighbor_helper<Metric, -1>(graph, points, k, cutoff, dimension, directed);
116
3.62k
    }
117
3.62k
}
nearest_neighbor.cpp:igraph_error_type_t dimension_dispatcher<nanoflann::L2_Adaptor<double, ig_point_adaptor, double, unsigned long> >(igraph_t*, igraph_matrix_t const*, long, double, long, bool)
Line
Count
Source
103
3.62k
        igraph_bool_t directed) {
104
105
3.62k
    switch (dimension) {
106
0
    case 0:
107
0
        IGRAPH_ERROR("0-dimensional points are not supported.", IGRAPH_EINVAL);
108
902
    case 1:
109
902
        return neighbor_helper<Metric, 1>(graph, points, k, cutoff, dimension, directed);
110
905
    case 2:
111
905
        return neighbor_helper<Metric, 2>(graph, points, k, cutoff, dimension, directed);
112
914
    case 3:
113
914
        return neighbor_helper<Metric, 3>(graph, points, k, cutoff, dimension, directed);
114
903
    default:
115
903
        return neighbor_helper<Metric, -1>(graph, points, k, cutoff, dimension, directed);
116
3.62k
    }
117
3.62k
}
Unexecuted instantiation: nearest_neighbor.cpp:igraph_error_type_t dimension_dispatcher<nanoflann::L1_Adaptor<double, ig_point_adaptor, double, unsigned long> >(igraph_t*, igraph_matrix_t const*, long, double, long, bool)
118
119
120
/**
121
 * \function igraph_nearest_neighbor_graph
122
 * \brief Computes the nearest neighbor graph for a spatial point set.
123
 *
124
 * \experimental
125
 *
126
 * This function constructs the \p k nearest neighbor graph of a given point
127
 * set. Each point is connected to at most \p k spatial neighbors within a
128
 * radius of \p cutoff.
129
 *
130
 * \param graph A pointer to the graph that will be created.
131
 * \param points A matrix containing the points that will be used to create
132
 *    the graph. Each row is a point, dimensionality is inferred from the
133
 *    column count.
134
 * \param metric The distance metric to use. See \ref igraph_metric_t.
135
 * \param k At most how many neighbors will be added for each vertex, set to
136
 *    a negative value to ignore.
137
 * \param cutoff Maximum distance at which connections will be made, set to a
138
 *    negative value or \c IGRAPH_INFINITY to ignore.
139
 * \param directed Whether to create a directed graph.
140
 * \return Error code.
141
 *
142
 * Time complexity: O(n log(n)) where n is the number of points.
143
 */
144
igraph_error_t igraph_nearest_neighbor_graph(igraph_t *graph,
145
        const igraph_matrix_t *points,
146
        igraph_metric_t metric,
147
        igraph_int_t k,
148
        igraph_real_t cutoff,
149
4.01k
        igraph_bool_t directed) {
150
151
4.01k
    const igraph_int_t dimension = igraph_matrix_ncol(points);
152
153
    // Negative cutoff values signify that no cutoff should be used.
154
4.01k
    cutoff = cutoff >= 0 ? cutoff : IGRAPH_INFINITY;
155
156
    // Handle null graph separately.
157
    // The number of matrix columns is not meaningful when there are zero rows.
158
    // This prevents throwing an error when both the column and the row counts are zero.
159
4.01k
    if (igraph_matrix_nrow(points) == 0) {
160
25
        return igraph_empty(graph, 0, directed);
161
25
    }
162
163
3.98k
    IGRAPH_CHECK(igraph_i_check_spatial_points(points));
164
165
3.62k
    IGRAPH_HANDLE_EXCEPTIONS_BEGIN;
166
167
3.62k
    switch (metric) {
168
3.62k
        case IGRAPH_METRIC_L2:
169
3.62k
            return dimension_dispatcher<nanoflann::L2_Adaptor<igraph_real_t, ig_point_adaptor> >(
170
3.62k
                    graph,
171
3.62k
                    points,
172
3.62k
                    k,
173
3.62k
                    cutoff * cutoff, // L2 uses square distances, so adjust for that here.
174
3.62k
                    dimension,
175
3.62k
                    directed);
176
0
        case IGRAPH_METRIC_L1:
177
0
            return dimension_dispatcher<nanoflann::L1_Adaptor<igraph_real_t, ig_point_adaptor> >(
178
0
                    graph,
179
0
                    points,
180
0
                    k,
181
0
                    cutoff,
182
0
                    dimension,
183
0
                    directed);
184
0
        default:
185
0
            IGRAPH_ERROR("Invalid metric.", IGRAPH_EINVAL);
186
3.62k
    }
187
188
3.62k
    IGRAPH_HANDLE_EXCEPTIONS_END;
189
0
}