/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(¤t_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, ¤t_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(¤t_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(¤t_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, ¤t_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(¤t_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(¤t_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, ¤t_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(¤t_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(¤t_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, ¤t_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(¤t_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(¤t_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, ¤t_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(¤t_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 | } |