MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
ProximityGraphs.cpp
Go to the documentation of this file.
1#include "ProximityGraphs.hpp"
2
3#ifdef MAYAFLUX_ARCH_X64
4#include <immintrin.h>
5#endif
6#ifdef MAYAFLUX_ARCH_ARM64
7#include <arm_neon.h>
8#endif
9
12
13#include <queue>
14
15namespace P = MayaFlux::Parallel;
16
17namespace MayaFlux::Kinesis {
18
19namespace {
20
21 /**
22 * @brief Largest point count for which the pairwise table is materialised.
23 *
24 * The table costs n^2 doubles and is only built for the cubic-time graphs,
25 * where it amortises to nothing. Above this the witness scans read the
26 * point coordinates directly.
27 */
28 constexpr size_t k_max_table_points = 8192;
29
30 /**
31 * @brief Squared distance between two column-major points.
32 * @param a First point, @p dim contiguous doubles.
33 * @param b Second point, @p dim contiguous doubles.
34 * @param dim Coordinate count.
35 */
36 [[nodiscard]] inline double dist_sq(const double* a, const double* b, size_t dim) noexcept
37 {
38 double sum = 0.0;
39 for (size_t c = 0; c < dim; ++c) {
40 const double delta = b[c] - a[c];
41 sum += delta * delta;
42 }
43 return sum;
44 }
45
46 /**
47 * @brief Whether any k satisfies row_i[k] + row_j[k] < threshold.
48 *
49 * The Gabriel rejection test. k equal to i or j can never satisfy it:
50 * both reduce to threshold < threshold, so the scan needs no exclusion.
51 */
52 [[nodiscard]] inline bool sum_below(
53 const double* row_i, const double* row_j, size_t n, double threshold) noexcept
54 {
55 size_t k = 0;
56
57#ifdef MAYAFLUX_ARCH_X64
58 const __m256d limit = _mm256_set1_pd(threshold);
59 for (; k + 4 <= n; k += 4) {
60 const __m256d sum = _mm256_add_pd(
61 _mm256_loadu_pd(row_i + k),
62 _mm256_loadu_pd(row_j + k));
63 if (_mm256_movemask_pd(_mm256_cmp_pd(sum, limit, _CMP_LT_OS)) != 0) {
64 return true;
65 }
66 }
67#elif defined(MAYAFLUX_ARCH_ARM64)
68 const float64x2_t limit = vdupq_n_f64(threshold);
69 for (; k + 2 <= n; k += 2) {
70 const float64x2_t sum = vaddq_f64(vld1q_f64(row_i + k), vld1q_f64(row_j + k));
71 const uint64x2_t mask = vcltq_f64(sum, limit);
72 if ((vgetq_lane_u64(mask, 0) | vgetq_lane_u64(mask, 1)) != 0) {
73 return true;
74 }
75 }
76#endif
77
78 for (; k < n; ++k) {
79 if (row_i[k] + row_j[k] < threshold) {
80 return true;
81 }
82 }
83 return false;
84 }
85
86 /**
87 * @brief Whether any k satisfies max(row_i[k], row_j[k]) < threshold.
88 *
89 * The relative neighborhood rejection test. As with sum_below, k equal to
90 * i or j reduces to threshold < threshold and cannot trigger.
91 */
92 [[nodiscard]] inline bool max_below(
93 const double* row_i, const double* row_j, size_t n, double threshold) noexcept
94 {
95 size_t k = 0;
96
97#ifdef MAYAFLUX_ARCH_X64
98 const __m256d limit = _mm256_set1_pd(threshold);
99 for (; k + 4 <= n; k += 4) {
100 const __m256d hi = _mm256_max_pd(
101 _mm256_loadu_pd(row_i + k),
102 _mm256_loadu_pd(row_j + k));
103 if (_mm256_movemask_pd(_mm256_cmp_pd(hi, limit, _CMP_LT_OS)) != 0) {
104 return true;
105 }
106 }
107#elif defined(MAYAFLUX_ARCH_ARM64)
108 const float64x2_t limit = vdupq_n_f64(threshold);
109 for (; k + 2 <= n; k += 2) {
110 const float64x2_t hi = vmaxq_f64(vld1q_f64(row_i + k), vld1q_f64(row_j + k));
111 const uint64x2_t mask = vcltq_f64(hi, limit);
112 if ((vgetq_lane_u64(mask, 0) | vgetq_lane_u64(mask, 1)) != 0) {
113 return true;
114 }
115 }
116#endif
117
118 for (; k < n; ++k) {
119 if (std::max(row_i[k], row_j[k]) < threshold) {
120 return true;
121 }
122 }
123 return false;
124 }
125
126 /**
127 * @brief Gabriel rejection test reading coordinates, for point counts
128 * above k_max_table_points.
129 */
130 [[nodiscard]] bool sum_below_direct(
131 const double* base, size_t dim, size_t n,
132 size_t i, size_t j, double threshold) noexcept
133 {
134 const double* pi = base + i * dim;
135 const double* pj = base + j * dim;
136
137 for (size_t k = 0; k < n; ++k) {
138 if (k == i || k == j) {
139 continue;
140 }
141 const double* pk = base + k * dim;
142 if (dist_sq(pi, pk, dim) + dist_sq(pj, pk, dim) < threshold) {
143 return true;
144 }
145 }
146 return false;
147 }
148
149 /**
150 * @brief Relative neighborhood rejection test reading coordinates, for
151 * point counts above k_max_table_points.
152 */
153 [[nodiscard]] bool max_below_direct(
154 const double* base, size_t dim, size_t n,
155 size_t i, size_t j, double threshold) noexcept
156 {
157 const double* pi = base + i * dim;
158 const double* pj = base + j * dim;
159
160 for (size_t k = 0; k < n; ++k) {
161 if (k == i || k == j) {
162 continue;
163 }
164 const double* pk = base + k * dim;
165 if (std::max(dist_sq(pi, pk, dim), dist_sq(pj, pk, dim)) < threshold) {
166 return true;
167 }
168 }
169 return false;
170 }
171
172 /**
173 * @brief Materialise the full pairwise squared distance table, row major.
174 * @return n*n doubles, or empty when n exceeds k_max_table_points.
175 *
176 * Only worth building for the cubic-time graphs. The quadratic ones pay
177 * the same order to build it as to run, so they read coordinates instead.
178 */
179 [[nodiscard]] std::vector<double> build_distance_table(const Eigen::MatrixXd& points)
180 {
181 const auto n = static_cast<size_t>(points.cols());
182 if (n > k_max_table_points) {
183 return {};
184 }
185
186 const auto dim = static_cast<size_t>(points.rows());
187 const double* base = points.data();
188
189 std::vector<double> table(n * n);
190
191 P::for_each(P::par_unseq,
192 std::views::iota(size_t { 0 }, n).begin(),
193 std::views::iota(size_t { 0 }, n).end(),
194 [&](size_t i) {
195 const double* pi = base + i * dim;
196 double* row = table.data() + i * n;
197 for (size_t j = 0; j < n; ++j) {
198 row[j] = dist_sq(pi, base + j * dim, dim);
199 }
200 });
201
202 return table;
203 }
204
205 /**
206 * @brief Concatenate per-source edge bins in ascending source order.
207 *
208 * Preserves the emission order of the serial nested loop, so the result
209 * is independent of how the parallel scheduler split the work.
210 */
211 [[nodiscard]] EdgeList flatten_bins(std::vector<EdgeList>& bins)
212 {
213 size_t total = 0;
214 for (const EdgeList& bin : bins) {
215 total += bin.size();
216 }
217
218 EdgeList edges;
219 edges.reserve(total);
220
221 for (EdgeList& bin : bins) {
222 edges.insert(edges.end(), bin.begin(), bin.end());
223 }
224
225 return edges;
226 }
227
228 struct Edge {
229 size_t a, b;
230 double weight;
231 bool operator>(const Edge& other) const { return weight > other.weight; }
232 };
233
234} // namespace
235
236EdgeList sequential_chain(const Eigen::MatrixXd& points)
237{
238 Eigen::Index n = points.cols();
239 if (n < 2) {
240 return {};
241 }
242
243 EdgeList edges;
244 edges.reserve(n - 1);
245
246 for (Eigen::Index i = 0; i < n - 1; ++i) {
247 edges.emplace_back(static_cast<size_t>(i), static_cast<size_t>(i + 1));
248 }
249
251 "sequential_chain: {} points, generated {} edges", n, edges.size());
252
253 return edges;
254}
255
257 const Eigen::MatrixXd& points,
258 size_t k)
259{
260 const auto n = static_cast<size_t>(points.cols());
261 if (n < 2) {
262 return {};
263 }
264
265 k = std::min(k, n - 1);
266 if (k == 0) {
267 return {};
268 }
269
270 const auto dim = static_cast<size_t>(points.rows());
271 const double* base = points.data();
272
273 EdgeList edges(n * k);
274
275 P::for_each(P::par_unseq,
276 std::views::iota(size_t { 0 }, n).begin(),
277 std::views::iota(size_t { 0 }, n).end(),
278 [&](size_t i) {
279 std::vector<std::pair<double, size_t>> distances;
280 distances.reserve(n - 1);
281
282 const double* pi = base + i * dim;
283 for (size_t j = 0; j < n; ++j) {
284 if (i == j) {
285 continue;
286 }
287 distances.emplace_back(dist_sq(pi, base + j * dim, dim), j);
288 }
289
290 std::partial_sort(
291 distances.begin(),
292 distances.begin() + static_cast<ptrdiff_t>(k),
293 distances.end());
294
295 for (size_t m = 0; m < k; ++m) {
296 edges[i * k + m] = { i, distances[m].second };
297 }
298 });
299
301 "k_nearest_neighbors: {} points, k={}, generated {} edges",
302 n, k, edges.size());
303
304 return edges;
305}
306
308 const Eigen::MatrixXd& points,
309 double radius)
310{
311 const auto n = static_cast<size_t>(points.cols());
312 if (n < 2) {
313 return {};
314 }
315
316 const double radius_sq = radius * radius;
317 const auto dim = static_cast<size_t>(points.rows());
318 const double* base = points.data();
319
320 std::vector<size_t> offsets(n + 1, 0);
321
322 P::for_each(P::par_unseq,
323 std::views::iota(size_t { 0 }, n).begin(),
324 std::views::iota(size_t { 0 }, n).end(),
325 [&](size_t i) {
326 const double* pi = base + i * dim;
327 size_t count = 0;
328 for (size_t j = i + 1; j < n; ++j) {
329 if (dist_sq(pi, base + j * dim, dim) <= radius_sq) {
330 ++count;
331 }
332 }
333 offsets[i + 1] = count;
334 });
335
336 for (size_t i = 0; i < n; ++i) {
337 offsets[i + 1] += offsets[i];
338 }
339
340 EdgeList edges(offsets[n]);
341
342 P::for_each(P::par_unseq,
343 std::views::iota(size_t { 0 }, n).begin(),
344 std::views::iota(size_t { 0 }, n).end(),
345 [&](size_t i) {
346 const double* pi = base + i * dim;
347 size_t at = offsets[i];
348 for (size_t j = i + 1; j < n; ++j) {
349 if (dist_sq(pi, base + j * dim, dim) <= radius_sq) {
350 edges[at++] = { i, j };
351 }
352 }
353 });
354
356 "radius_threshold_graph: {} points, radius={:.3f}, generated {} edges",
357 n, radius, edges.size());
358
359 return edges;
360}
361
362EdgeList minimum_spanning_tree(const Eigen::MatrixXd& points)
363{
364 const auto n = static_cast<size_t>(points.cols());
365 if (n < 2) {
366 return {};
367 }
368
369 const auto dim = static_cast<size_t>(points.rows());
370 const double* base = points.data();
371
372 EdgeList mst_edges;
373 mst_edges.reserve(n - 1);
374
375 std::vector<bool> in_mst(n, false);
376 std::priority_queue<Edge, std::vector<Edge>, std::greater<>> pq;
377
378 in_mst[0] = true;
379
380 for (size_t j = 1; j < n; ++j) {
381 pq.push({ .a = 0, .b = j, .weight = std::sqrt(dist_sq(base, base + j * dim, dim)) });
382 }
383
384 while (!pq.empty() && mst_edges.size() < n - 1) {
385 const Edge e = pq.top();
386 pq.pop();
387
388 if (in_mst[e.b]) {
389 continue;
390 }
391
392 mst_edges.emplace_back(e.a, e.b);
393 in_mst[e.b] = true;
394
395 const double* pb = base + e.b * dim;
396 for (size_t j = 0; j < n; ++j) {
397 if (!in_mst[j]) {
398 pq.push({ .a = e.b, .b = j, .weight = std::sqrt(dist_sq(pb, base + j * dim, dim)) });
399 }
400 }
401 }
402
404 "minimum_spanning_tree: {} points, generated {} edges",
405 n, mst_edges.size());
406
407 return mst_edges;
408}
409
410EdgeList gabriel_graph(const Eigen::MatrixXd& points)
411{
412 const auto n = static_cast<size_t>(points.cols());
413 if (n < 2) {
414 return {};
415 }
416
417 const auto dim = static_cast<size_t>(points.rows());
418 const double* base = points.data();
419
420 const std::vector<double> table = build_distance_table(points);
421 const bool resident = !table.empty();
422
423 std::vector<EdgeList> bins(n);
424
425 P::for_each(P::par_unseq,
426 std::views::iota(size_t { 0 }, n).begin(),
427 std::views::iota(size_t { 0 }, n).end(),
428 [&](size_t i) {
429 const double* row_i = resident ? table.data() + i * n : nullptr;
430 const double* pi = base + i * dim;
431 EdgeList& out = bins[i];
432
433 for (size_t j = i + 1; j < n; ++j) {
434 const double pq_sq = resident
435 ? row_i[j]
436 : dist_sq(pi, base + j * dim, dim);
437
438 const bool rejected = resident
439 ? sum_below(row_i, table.data() + j * n, n, pq_sq)
440 : sum_below_direct(base, dim, n, i, j, pq_sq);
441
442 if (!rejected) {
443 out.emplace_back(i, j);
444 }
445 }
446 });
447
448 EdgeList edges = flatten_bins(bins);
449
451 "gabriel_graph: {} points, generated {} edges",
452 n, edges.size());
453
454 return edges;
455}
456
458{
459 const auto n = static_cast<size_t>(points.cols());
460 if (n < 2) {
461 return {};
462 }
463
464 const auto dim = static_cast<size_t>(points.rows());
465 const double* base = points.data();
466
467 std::vector<size_t> nearest(n);
468
469 P::for_each(P::par_unseq,
470 std::views::iota(size_t { 0 }, n).begin(),
471 std::views::iota(size_t { 0 }, n).end(),
472 [&](size_t i) {
473 double min_dist_sq = std::numeric_limits<double>::max();
474 size_t best = i;
475
476 const double* pi = base + i * dim;
477 for (size_t j = 0; j < n; ++j) {
478 if (i == j) {
479 continue;
480 }
481 const double d = dist_sq(pi, base + j * dim, dim);
482 if (d < min_dist_sq) {
483 min_dist_sq = d;
484 best = j;
485 }
486 }
487
488 nearest[i] = best;
489 });
490
491 EdgeList edges;
492 edges.reserve(n);
493
494 for (size_t i = 0; i < n; ++i) {
495 if (nearest[i] != i) {
496 edges.emplace_back(i, nearest[i]);
497 }
498 }
499
501 "nearest_neighbor_graph: {} points, generated {} edges", n, edges.size());
502
503 return edges;
504}
505
507{
508 const auto n = static_cast<size_t>(points.cols());
509 if (n < 2) {
510 return {};
511 }
512
513 const auto dim = static_cast<size_t>(points.rows());
514 const double* base = points.data();
515
516 const std::vector<double> table = build_distance_table(points);
517 const bool resident = !table.empty();
518
519 std::vector<EdgeList> bins(n);
520
521 P::for_each(P::par_unseq,
522 std::views::iota(size_t { 0 }, n).begin(),
523 std::views::iota(size_t { 0 }, n).end(),
524 [&](size_t i) {
525 const double* row_i = resident ? table.data() + i * n : nullptr;
526 const double* pi = base + i * dim;
527 EdgeList& out = bins[i];
528
529 for (size_t j = i + 1; j < n; ++j) {
530 const double pq_sq = resident
531 ? row_i[j]
532 : dist_sq(pi, base + j * dim, dim);
533
534 const bool rejected = resident
535 ? max_below(row_i, table.data() + j * n, n, pq_sq)
536 : max_below_direct(base, dim, n, i, j, pq_sq);
537
538 if (!rejected) {
539 out.emplace_back(i, j);
540 }
541 }
542 });
543
544 EdgeList edges = flatten_bins(bins);
545
547 "relative_neighborhood_graph: {} points, generated {} edges",
548 n, edges.size());
549
550 return edges;
551}
552
554 const Eigen::MatrixXd& points,
555 const std::function<EdgeList(const Eigen::MatrixXd&)>& connection_function)
556{
557 if (!connection_function) {
559 "custom_proximity_graph: connection_function is null");
560 return {};
561 }
562
563 EdgeList edges = connection_function(points);
564
566 "custom_proximity_graph: {} points, generated {} edges",
567 points.cols(), edges.size());
568
569 return edges;
570}
571
573 const Eigen::MatrixXd& points,
574 const ProximityConfig& config)
575{
576 switch (config.mode) {
577
579 return sequential_chain(points);
580
582 return k_nearest_neighbors(points, config.k_neighbors);
583
585 return radius_threshold_graph(points, config.radius);
586
589
591 return gabriel_graph(points);
592
595
598
601
602 default:
603 return {};
604 }
605}
606
607} // namespace MayaFlux::Kinesis
#define MF_ERROR(comp, ctx,...)
#define MF_DEBUG(comp, ctx,...)
std::vector< glm::vec2 > * points
float radius
size_t a
double weight
size_t b
size_t count
float threshold
float k
float hi
@ Runtime
General runtime operations (default fallback)
@ Kinesis
General mathematical and physics algorithns.
std::vector< double > sum(std::span< const double > data, size_t n_windows, uint32_t hop_size, uint32_t window_size)
Sum per window.
Definition Analysis.cpp:469
EdgeList sequential_chain(const Eigen::MatrixXd &points)
Compute sequential chain graph.
EdgeList custom_proximity_graph(const Eigen::MatrixXd &points, const std::function< EdgeList(const Eigen::MatrixXd &)> &connection_function)
Custom proximity graph via user function.
EdgeList relative_neighborhood_graph(const Eigen::MatrixXd &points)
Compute relative neighborhood graph.
EdgeList minimum_spanning_tree(const Eigen::MatrixXd &points)
Compute minimum spanning tree (Prim's algorithm)
EdgeList gabriel_graph(const Eigen::MatrixXd &points)
Compute Gabriel graph.
EdgeList k_nearest_neighbors(const Eigen::MatrixXd &points, size_t k)
Compute K-nearest neighbors graph.
EdgeList generate_proximity_graph(const Eigen::MatrixXd &points, const ProximityConfig &config)
Generate proximity graph using specified mode.
EdgeList radius_threshold_graph(const Eigen::MatrixXd &points, double radius)
Compute radius threshold graph.
std::vector< std::pair< size_t, size_t > > EdgeList
EdgeList nearest_neighbor_graph(const Eigen::MatrixXd &points)
Compute nearest neighbor graph.
std::function< EdgeList(const Eigen::MatrixXd &)> custom_function