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