Loading...
Searching...
No Matches
choose_n_farthest_points.h
1/* This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT.
2 * See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details.
3 * Author(s): Siargey Kachanovich, Marc Glisse
4 *
5 * Copyright (C) 2016 Inria
6 *
7 * Modification(s):
8 * - 2022/11 Glisse: New *_metric variant
9 * - 2026/04 Vincent Rouvreau: Use Gudhi::random
10 * - YYYY/MM Author: Description of the modification
11 */
12
13#ifndef CHOOSE_N_FARTHEST_POINTS_H_
14#define CHOOSE_N_FARTHEST_POINTS_H_
15
16#include <boost/version.hpp>
17#include <boost/range.hpp>
18#include <boost/heap/d_ary_heap.hpp>
19#if BOOST_VERSION >= 108100
20 #include <boost/unordered/unordered_flat_set.hpp>
21#else
22 #include <boost/unordered_set.hpp> // preferably with boost 1.79+ for speed
23#endif
24
25#include <gudhi/Null_output_iterator.h>
26#include <gudhi/random.h>
27
28#include <iterator>
29#include <vector>
30#include <utility>
31#include <limits>
32
33namespace Gudhi {
34
35namespace subsampling {
36
40enum : std::size_t {
44 random_starting_point = std::size_t(-1)
45};
46
72template < typename Distance,
73typename Point_range,
74typename PointOutputIterator,
75typename DistanceOutputIterator = Null_output_iterator>
76void choose_n_farthest_points(Distance dist,
77 Point_range const &input_pts,
78 std::size_t final_size,
79 std::size_t starting_point,
80 PointOutputIterator output_it,
81 DistanceOutputIterator dist_it = {}) {
82 std::size_t nb_points = boost::size(input_pts);
83 if (final_size > nb_points)
84 final_size = nb_points;
85
86 // Tests to the limit
87 if (final_size < 1)
88 return;
89
90 if (starting_point == random_starting_point) {
91 // Choose randomly the first landmark
92 starting_point = Gudhi::random::get_uniform<std::size_t>(0, nb_points - 1);
93 }
94
95 // FIXME: don't hard-code the type as double. For Epeck_d, we also want to handle types that do not have an infinity.
96 typedef double FT;
97 static_assert(std::numeric_limits<FT>::has_infinity, "the number type needs to support infinity()");
98
99 *output_it++ = input_pts[starting_point];
100 *dist_it++ = std::numeric_limits<FT>::infinity();
101 if (final_size == 1) return;
102
103 std::vector<std::size_t> points(nb_points); // map from remaining points to indexes in input_pts
104 std::vector< FT > dist_to_L(nb_points); // vector of current distances to L from points
105 for(std::size_t i = 0; i < nb_points; ++i) {
106 points[i] = i;
107 dist_to_L[i] = dist(input_pts[i], input_pts[starting_point]);
108 }
109 // The indirection through points makes the program a bit slower. Some alternatives:
110 // - the original code never removed points and counted on them not
111 // reappearing because of a self-distance of 0. This causes unnecessary
112 // computations when final_size is large. It also causes trouble if there are
113 // input points at distance 0 from each other.
114 // - copy input_pts and update the local copy when removing points.
115
116 std::size_t curr_max_w = starting_point;
117
118 for (std::size_t current_number_of_landmarks = 1; current_number_of_landmarks != final_size; current_number_of_landmarks++) {
119 std::size_t latest_landmark = points[curr_max_w];
120 // To remove the latest landmark at index curr_max_w, replace it
121 // with the last point and reduce the length of the vector.
122 std::size_t last = points.size() - 1;
123 if (curr_max_w != last) {
124 points[curr_max_w] = points[last];
125 dist_to_L[curr_max_w] = dist_to_L[last];
126 }
127 points.pop_back();
128
129 // Update distances to L.
130 std::size_t i = 0;
131 for (auto p : points) {
132 FT curr_dist = dist(input_pts[p], input_pts[latest_landmark]);
133 if (curr_dist < dist_to_L[i])
134 dist_to_L[i] = curr_dist;
135 ++i;
136 }
137 // choose the next landmark
138 curr_max_w = 0;
139 FT curr_max_dist = dist_to_L[curr_max_w]; // used for defining the furthest point from L
140 for (i = 1; i < points.size(); i++)
141 if (dist_to_L[i] > curr_max_dist) {
142 curr_max_dist = dist_to_L[i];
143 curr_max_w = i;
144 }
145 *output_it++ = input_pts[points[curr_max_w]];
146 *dist_it++ = dist_to_L[curr_max_w];
147 }
148}
149
150
151// How bad is it to use the triangle inequality with inexact double computations?
152// Hopefully we still get a net-tree.
153
154template<class FT> struct Landmark_info;
155template<class FT>
156struct Compare_landmark_radius {
157 std::vector<Landmark_info<FT>>* landmarks_p;
158 Compare_landmark_radius(std::vector<Landmark_info<FT>>* p): landmarks_p(p) {}
159 bool operator()(std::size_t, std::size_t) const;
160};
161// I compared all the heaps in boost. Fibonacci is not bad, but d_ary is by far the fastest.
162// Arity of 3 is clearly faster than 2, and speed doesn't change much when increasing the arity even more.
163template<class FT>
164using radius_priority_ds =
165 boost::heap::d_ary_heap<std::size_t, boost::heap::arity<7>, boost::heap::compare<Compare_landmark_radius<FT>>,
166 boost::heap::mutable_<true>, boost::heap::constant_time_size<false>>;
167template<class FT>
168struct Landmark_info {
169 std::size_t farthest; FT radius;
170 // The points that are closer to this landmark than to other landmarks
171 std::vector<std::pair<std::size_t, FT>> voronoi;
172 // For a landmark A, the list of landmarks B such that picking a Voronoi
173 // point of A as a new landmark might steal a Voronoi point from B, or vice versa.
174 std::vector<std::pair<std::size_t, FT>> neighbors;
175 // Note that above we cache the distances. This is always good for Voronoi, and for neighbors it is neutral in 2D and helps in 4D.
176 typename radius_priority_ds<FT>::handle_type position_in_queue;
177};
178template<class FT>
179bool Compare_landmark_radius<FT>::operator()(std::size_t a, std::size_t b)const{ return (*landmarks_p)[a].radius < (*landmarks_p)[b].radius; }
180
202template < typename Distance,
203typename Point_range,
204typename PointOutputIterator,
205typename DistanceOutputIterator = Null_output_iterator>
207 Point_range const &input_pts,
208 std::size_t final_size,
209 std::size_t starting_point,
210 PointOutputIterator output_it,
211 DistanceOutputIterator dist_it = {}) {
212 std::size_t nb_points = boost::size(input_pts);
213 if (final_size > nb_points)
214 final_size = nb_points;
215
216 // Tests to the limit
217 if (final_size < 1)
218 return;
219
220 if (starting_point == random_starting_point) {
221 // Choose randomly the first landmark
222 starting_point = Gudhi::random::get_uniform<std::size_t>(0, nb_points - 1);
223 }
224
225 // FIXME: don't hard-code the type as double. For Epeck_d, we also want to handle types that do not have an infinity.
226 typedef double FT;
227 static_assert(std::numeric_limits<FT>::has_infinity, "the number type needs to support infinity()");
228
229 *output_it++ = input_pts[starting_point];
230 *dist_it++ = std::numeric_limits<FT>::infinity();
231 if (final_size == 1) return;
232
233 auto dist = [&](std::size_t a, std::size_t b){ return dist_(input_pts[a], input_pts[b]); };
234
235 std::vector<Landmark_info<FT>> landmarks(nb_points);
236 radius_priority_ds<FT> radius_priority(&landmarks);
237
238 auto compute_radius = [&](std::size_t i)
239 {
240 FT r = -std::numeric_limits<FT>::infinity(); // -2 * diameter should suffice
241 std::size_t jmax = -1;
242 for(auto [ j, d ] : landmarks[i].voronoi) {
243 if (d > r) {
244 r = d;
245 jmax = j;
246 }
247 }
248 landmarks[i].radius = r;
249 landmarks[i].farthest = jmax;
250 };
251 auto update_radius = [&](std::size_t i)
252 {
253 compute_radius(i);
254 radius_priority.decrease(landmarks[i].position_in_queue);
255 };
256
257 {
258 // Initialize everything with starting_point
259 auto& ini = landmarks[starting_point];
260 ini.voronoi.reserve(nb_points - 1);
261 for (std::size_t i = 0; i < nb_points; ++i)
262 if (i != starting_point)
263 ini.voronoi.emplace_back(i, dist(starting_point, i));
264 compute_radius(starting_point);
265 ini.position_in_queue = radius_priority.push(starting_point);
266 }
267 // outside the loop to recycle the allocation
268 std::vector<std::size_t> modified_neighbors;
269#if BOOST_VERSION >= 108100
270 boost::unordered_flat_set<std::size_t>
271#else
272 boost::unordered_set<std::size_t>
273#endif
274 l_neighbors; // Should we use an allocator?
275 for (std::size_t current_number_of_landmarks = 1; current_number_of_landmarks != final_size; current_number_of_landmarks++) {
276 std::size_t l_parent = radius_priority.top();
277 auto& parent_info = landmarks[l_parent];
278 std::size_t l = parent_info.farthest;
279 FT radius = parent_info.radius;
280 auto& info = landmarks[l];
281 *output_it++ = input_pts[l];
282 *dist_it++ = radius;
283 l_neighbors.clear();
284 modified_neighbors.clear();
285 // If a Voronoi point X of A can steal a Voronoi point Y from B, then
286 // BY > XY >= AB - AX - BY, so AB < AX + 2 * BY. Symmetrized.
287 auto max_dist = [](FT a, FT b){ return a + b + std::max(a, b); }; // tighter than 3 * radius
288 // Check if any Voronoi points from ngb need to move to l
289 auto handle_neighbor_voronoi = [&](std::size_t ngb)
290 {
291 auto& ngb_info = landmarks[ngb];
292 auto it = std::remove_if(ngb_info.voronoi.begin(), ngb_info.voronoi.end(), [&](auto wd)
293 {
294 std::size_t w = wd.first;
295 FT d = wd.second;
296 FT newd = dist(l, w);
297 if (newd < d) {
298 if (w != l) // w==l can only happen for ngb==l_parent
299 info.voronoi.emplace_back(w, newd);
300 return true;
301 }
302 return false;
303 });
304 if (it != ngb_info.voronoi.end()) { // modified, always true for ngb==l_parent
305 ngb_info.voronoi.erase(it, ngb_info.voronoi.end());
306 modified_neighbors.push_back(ngb);
307 // We only need to recompute the radius if farthest was removed, which we can test here with
308 // if (dist(l, ngb_info.farthest) < ngb_info.radius)
309 // to avoid a costly test for each w in the loop above, but it does not seem to help.
310 update_radius(ngb);
311 // if (ngb_info.voronoi.empty()) radius_priority.erase(ngb_info.position_in_queue);
312 }
313 // We could easily return true/false here to say whether anything was modified, if useful.
314 };
315 auto handle_neighbor_neighbors = [&](std::size_t ngb)
316 {
317 auto& ngb_info = landmarks[ngb];
318 auto it = std::remove_if(ngb_info.neighbors.begin(), ngb_info.neighbors.end(), [&](auto near_){
319 std::size_t near = near_.first;
320 FT d = near_.second;
321 // Conservative 3 * radius: we could use the old radii of ngb and near, but not the new ones.
322 if (d <= 3 * radius) { l_neighbors.insert(near); }
323 // Here it is safe to use the new radii.
324 return d >= max_dist(ngb_info.radius, landmarks[near].radius);
325 });
326 ngb_info.neighbors.erase(it, ngb_info.neighbors.end());
327 };
328 // First update the Voronoi diagram, so we can compute all the updated
329 // radii before pruning neighbor lists. The main drawback is that we have
330 // to store modified_neighbors, and we don't have access to the old radii
331 // in handle_neighbor_neighbors.
332 handle_neighbor_voronoi(l_parent);
333 // Should we make this loop a remove_if? We already remove in the next loop.
334 for (auto ngb_ : parent_info.neighbors) {
335 std::size_t ngb = ngb_.first;
336 //if(ngb_.second <= max_dist(radius, landmarks[ngb].radius)) // radius from before update_radius(l_parent)
337 //if(ngb_.second <= radius + 2 * landmarks[ngb].radius) // no need to symmetrize
338 // If X can steal a Voronoi point Y from B, then BY > XY >= BX - BY, so BX < 2 * BY.
339 if(dist(l, ngb) < 2 * landmarks[ngb].radius)
340 handle_neighbor_voronoi(ngb);
341 }
342 // If there are too many neighbors (of neighbors), this could be quadratic (?).
343 // Testing every landmark would then be faster, linear.
344 // TODO: find a good heuristic to switch to the linear case.
345 for (std::size_t ngb : modified_neighbors)
346 handle_neighbor_neighbors(ngb);
347 // Finalize the new Voronoi cell.
348 compute_radius(l);
349 info.position_in_queue = radius_priority.push(l);
350 // The parent is an obvious candidate to be a neighbor.
351 l_neighbors.insert(l_parent);
352 for (std::size_t ngb : l_neighbors) {
353 FT d = dist(l, ngb);
354 if (d < max_dist(info.radius, landmarks[ngb].radius)) {
355 info.neighbors.emplace_back(ngb, d);
356 landmarks[ngb].neighbors.emplace_back(l, d);
357 }
358 }
359 }
360}
361
362} // namespace subsampling
363
364} // namespace Gudhi
365
366#endif // CHOOSE_N_FARTHEST_POINTS_H_
Type get_uniform(const Type &min, const Type &max, CustomRandomGenerator &&rng=get_default_random())
Generates a random number in the range [min, max].
Definition random.h:117
void choose_n_farthest_points(Distance dist, Point_range const &input_pts, std::size_t final_size, std::size_t starting_point, PointOutputIterator output_it, DistanceOutputIterator dist_it={})
Subsample by an iterative, greedy strategy.
Definition choose_n_farthest_points.h:76
void choose_n_farthest_points_metric(Distance dist_, Point_range const &input_pts, std::size_t final_size, std::size_t starting_point, PointOutputIterator output_it, DistanceOutputIterator dist_it={})
Subsample by an iterative, greedy strategy.
Definition choose_n_farthest_points.h:206
@ random_starting_point
Definition choose_n_farthest_points.h:44
Gudhi namespace.
Definition SimplicialComplexForAlpha.h:14
Definition Null_output_iterator.h:19