MayaFlux 0.4.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
SpatialIndex.cpp
Go to the documentation of this file.
1#include "SpatialIndex.hpp"
2
4
5#include <queue>
6
7namespace MayaFlux::Kinesis {
8
9// =========================================================================
10// Point-type traits (internal)
11// =========================================================================
12
13namespace detail {
14
15 template <typename PointT>
17
18 template <>
19 struct PointTraits<glm::vec3> {
20 static constexpr uint32_t dimensions = 3;
21 static constexpr bool fixed_dimension = true;
22
23 static float sq_distance_euclidean(const glm::vec3& a, const glm::vec3& b)
24 {
25 glm::vec3 d = a - b;
26 return glm::dot(d, d);
27 }
28 };
29
30 template <>
31 struct PointTraits<Eigen::VectorXd> {
32 static constexpr bool fixed_dimension = false;
33
34 static float sq_distance_euclidean(const Eigen::VectorXd& a, const Eigen::VectorXd& b)
35 {
36 return static_cast<float>((a - b).squaredNorm());
37 }
38
39 static uint32_t dimensions(const Eigen::VectorXd& p)
40 {
41 return static_cast<uint32_t>(p.size());
42 }
43 };
44
45 template <typename PointT>
46 uint32_t get_dimensions([[maybe_unused]] const PointT& p)
47 {
50 } else {
52 }
53 }
54
55 constexpr uint32_t MAX_GRID_DIMENSIONS = 6;
56
57} // namespace detail
58
59// =========================================================================
60// Construction / destruction
61// =========================================================================
62
63template <typename PointT>
65 : m_cell_size(cell_size)
66 , m_inv_cell(1.0F / cell_size)
67 , m_distance_fn(std::move(distance))
68 , m_use_grid(static_cast<bool>(detail::PointTraits<PointT>::fixed_dimension))
69{
70
71#ifdef MAYAFLUX_PLATFORM_MACOS
72 for (auto& hp : m_hazard_ptrs) {
73 hp.store(nullptr);
74 }
75#endif
76}
77
78template <typename PointT>
80{
81#ifdef MAYAFLUX_PLATFORM_MACOS
82 auto* snap = m_snapshot.load(std::memory_order_acquire);
83 delete snap;
84 for (auto* r : m_retired) {
85 delete r;
86 }
87#endif
88}
89
90// =========================================================================
91// Mutation
92// =========================================================================
93
94template <typename PointT>
96{
97 uint32_t id = m_next_id++;
98 uint32_t slot {};
99
100 if (!m_free_slots.empty()) {
101 slot = m_free_slots.back();
102 m_free_slots.pop_back();
103 m_positions[slot] = position;
104 m_slot_to_id[slot] = id;
105 } else {
106 slot = static_cast<uint32_t>(m_positions.size());
107 m_positions.push_back(position);
108 m_slot_to_id.push_back(id);
109 }
110
111 m_id_to_slot[id] = slot;
112
114 if (m_positions.size() == 1) {
115 uint32_t dims = detail::get_dimensions(position);
116 m_use_grid = dims <= detail::MAX_GRID_DIMENSIONS;
117 }
118 }
119
120 return id;
121}
122
123template <typename PointT>
124void SpatialIndex<PointT>::update(uint32_t id, const PointT& position)
125{
126 auto it = m_id_to_slot.find(id);
127 if (it == m_id_to_slot.end()) {
129 "SpatialIndex::update: unknown id {}", id);
130 return;
131 }
132 m_positions[it->second] = position;
133}
134
135template <typename PointT>
137{
138 auto it = m_id_to_slot.find(id);
139 if (it == m_id_to_slot.end()) {
141 "SpatialIndex::remove: unknown id {}", id);
142 return;
143 }
144
145 uint32_t slot = it->second;
146 m_free_slots.push_back(slot);
147 m_slot_to_id[slot] = UINT32_MAX;
148 m_id_to_slot.erase(it);
149}
150
151template <typename PointT>
153{
154 m_positions.clear();
155 m_id_to_slot.clear();
156 m_slot_to_id.clear();
157 m_free_slots.clear();
158}
159
160// =========================================================================
161// Publication
162// =========================================================================
163
164template <typename PointT>
166{
167 auto snap = std::make_unique<SpatialSnapshot<PointT>>();
168 snap->cell_size = m_cell_size;
169
172 } else {
173 snap->dimensions = m_positions.empty()
174 ? 0
175 : detail::get_dimensions(m_positions.front());
176 }
177
178 auto live_count = static_cast<uint32_t>(m_id_to_slot.size());
179 snap->positions.reserve(live_count);
180 snap->slot_to_id.reserve(live_count);
181 snap->id_to_slot.reserve(live_count);
182
183 for (const auto& [id, slot] : m_id_to_slot) {
184 auto new_slot = static_cast<uint32_t>(snap->positions.size());
185 snap->positions.push_back(m_positions[slot]);
186 snap->slot_to_id.push_back(id);
187 snap->id_to_slot[id] = new_slot;
188 }
189
190 if (m_use_grid) {
191 rebuild_grid(*snap);
192 }
193
194#ifdef MAYAFLUX_PLATFORM_MACOS
195 auto* old = m_snapshot.exchange(snap.release(), std::memory_order_acq_rel);
196 if (old) {
197 retire_snapshot(old);
198 }
199#else
200 m_snapshot.store(
201 std::shared_ptr<const SpatialSnapshot<PointT>>(snap.release()),
202 std::memory_order_release);
203#endif
204}
205
206// =========================================================================
207// Grid construction
208// =========================================================================
209
210template <typename PointT>
212{
213 snap.cells.clear();
214 for (uint32_t i = 0; i < static_cast<uint32_t>(snap.positions.size()); ++i) {
215 uint64_t h = hash_cell(snap.positions[i]);
216 snap.cells[h].push_back(i);
217 }
218}
219
220template <typename PointT>
221uint64_t SpatialIndex<PointT>::hash_cell(const PointT& p) const
222{
223 if constexpr (std::is_same_v<PointT, glm::vec3>) {
224 auto [cx, cy, cz] = detail::cell_coords_3d(p, m_inv_cell);
225 return detail::hash_cell_3d(cx, cy, cz);
226 } else {
227 return detail::hash_cell_nd(p, m_inv_cell);
228 }
229}
230
231// =========================================================================
232// Queries
233// =========================================================================
234
235template <typename PointT>
236std::vector<QueryResult> SpatialIndex<PointT>::within_radius(
237 const PointT& center,
238 float radius) const
239{
240 std::vector<QueryResult> results;
241 float radius_sq = radius * radius;
242
243#ifdef MAYAFLUX_PLATFORM_MACOS
244 auto [snap, slot] = acquire_snapshot();
245 if (!snap) {
246 return results;
247 }
248#else
249 auto snap_ptr = m_snapshot.load(std::memory_order_acquire);
250 if (!snap_ptr) {
251 return results;
252 }
253 const auto* snap = snap_ptr.get();
254#endif
255
256 if (m_use_grid) {
257 query_grid(*snap, center, radius_sq, results);
258 } else {
259 query_brute(*snap, center, radius_sq, results);
260 }
261
262#ifdef MAYAFLUX_PLATFORM_MACOS
263 release_snapshot(slot);
264#endif
265
266 return results;
267}
268
269template <typename PointT>
270std::vector<QueryResult> SpatialIndex<PointT>::k_nearest(
271 const PointT& center,
272 uint32_t k) const
273{
274#ifdef MAYAFLUX_PLATFORM_MACOS
275 auto [snap, slot] = acquire_snapshot();
276 if (!snap || snap->positions.empty()) {
277 if (snap)
278 release_snapshot(slot);
279 return {};
280 }
281#else
282 auto snap_ptr = m_snapshot.load(std::memory_order_acquire);
283 if (!snap_ptr || snap_ptr->positions.empty()) {
284 return {};
285 }
286 const auto* snap = snap_ptr.get();
287#endif
288
289 k = std::min(k, static_cast<uint32_t>(snap->positions.size()));
290
291 auto cmp = [](const QueryResult& a, const QueryResult& b) {
292 return a.distance_sq < b.distance_sq;
293 };
294 std::priority_queue<QueryResult, std::vector<QueryResult>, decltype(cmp)> heap(cmp);
295
296 for (uint32_t i = 0; i < static_cast<uint32_t>(snap->positions.size()); ++i) {
297 float d_sq = m_distance_fn(center, snap->positions[i]);
298 if (heap.size() < k) {
299 heap.push({ snap->slot_to_id[i], d_sq });
300 } else if (d_sq < heap.top().distance_sq) {
301 heap.pop();
302 heap.push({ snap->slot_to_id[i], d_sq });
303 }
304 }
305
306 std::vector<QueryResult> results;
307 results.reserve(heap.size());
308 while (!heap.empty()) {
309 results.push_back(heap.top());
310 heap.pop();
311 }
312 std::ranges::reverse(results);
313
314#ifdef MAYAFLUX_PLATFORM_MACOS
315 release_snapshot(slot);
316#endif
317
318 return results;
319}
320
321template <typename PointT>
322std::optional<PointT> SpatialIndex<PointT>::position_of(uint32_t id) const
323{
324#ifdef MAYAFLUX_PLATFORM_MACOS
325 auto [snap, slot] = acquire_snapshot();
326 if (!snap) {
327 return std::nullopt;
328 }
329 auto it = snap->id_to_slot.find(id);
330 std::optional<PointT> result = (it != snap->id_to_slot.end())
331 ? std::optional<PointT>(snap->positions[it->second])
332 : std::nullopt;
333 release_snapshot(slot);
334 return result;
335#else
336 auto snap_ptr = m_snapshot.load(std::memory_order_acquire);
337 if (!snap_ptr) {
338 return std::nullopt;
339 }
340 auto it = snap_ptr->id_to_slot.find(id);
341 if (it == snap_ptr->id_to_slot.end()) {
342 return std::nullopt;
343 }
344 return snap_ptr->positions[it->second];
345#endif
346}
347
348template <typename PointT>
350{
351#ifdef MAYAFLUX_PLATFORM_MACOS
352 auto [snap, slot] = acquire_snapshot();
353 size_t n = snap ? snap->positions.size() : 0;
354 if (snap)
355 release_snapshot(slot);
356 return n;
357#else
358 auto snap_ptr = m_snapshot.load(std::memory_order_acquire);
359 return snap_ptr ? snap_ptr->positions.size() : 0;
360#endif
361}
362
363template <typename PointT>
364std::vector<std::pair<uint32_t, PointT>> SpatialIndex<PointT>::all() const
365{
366 std::vector<std::pair<uint32_t, PointT>> result;
367
368#ifdef MAYAFLUX_PLATFORM_MACOS
369 auto [snap, slot] = acquire_snapshot();
370 if (!snap) {
371 return result;
372 }
373 result.reserve(snap->positions.size());
374 for (uint32_t i = 0; i < static_cast<uint32_t>(snap->positions.size()); ++i) {
375 result.emplace_back(snap->slot_to_id[i], snap->positions[i]);
376 }
377 release_snapshot(slot);
378#else
379 auto snap_ptr = m_snapshot.load(std::memory_order_acquire);
380 if (!snap_ptr) {
381 return result;
382 }
383 result.reserve(snap_ptr->positions.size());
384 for (uint32_t i = 0; i < static_cast<uint32_t>(snap_ptr->positions.size()); ++i) {
385 result.emplace_back(snap_ptr->slot_to_id[i], snap_ptr->positions[i]);
386 }
387#endif
388
389 return result;
390}
391
392// =========================================================================
393// Grid query
394// =========================================================================
395
396template <typename PointT>
398 const SpatialSnapshot<PointT>& snap,
399 const PointT& center,
400 float radius_sq,
401 std::vector<QueryResult>& out) const
402{
403 auto extent = static_cast<int32_t>(std::ceil(std::sqrt(radius_sq) * m_inv_cell));
404
405 auto check_cell = [&](uint64_t h) {
406 auto it = snap.cells.find(h);
407 if (it == snap.cells.end()) {
408 return;
409 }
410 for (uint32_t slot : it->second) {
411 float d_sq = m_distance_fn(center, snap.positions[slot]);
412 if (d_sq <= radius_sq) {
413 out.push_back({ snap.slot_to_id[slot], d_sq });
414 }
415 }
416 };
417
418 if constexpr (std::is_same_v<PointT, glm::vec3>) {
419 auto [cx, cy, cz] = detail::cell_coords_3d(center, m_inv_cell);
420 for (int32_t dx = -extent; dx <= extent; ++dx) {
421 for (int32_t dy = -extent; dy <= extent; ++dy) {
422 for (int32_t dz = -extent; dz <= extent; ++dz) {
423 check_cell(detail::hash_cell_3d(cx + dx, cy + dy, cz + dz));
424 }
425 }
426 }
427 } else {
428 uint32_t dims = snap.dimensions;
429 std::vector<int32_t> base_cell(dims);
430 for (uint32_t d = 0; d < dims; ++d) {
431 base_cell[d] = static_cast<int32_t>(std::floor(center(d) * m_inv_cell));
432 }
433
434 std::vector<int32_t> offsets(dims, -extent);
435
436 auto advance = [&]() -> bool {
437 for (uint32_t d = 0; d < dims; ++d) {
438 if (++offsets[d] <= extent) {
439 return true;
440 }
441 offsets[d] = -extent;
442 }
443 return false;
444 };
445
446 do {
447 Eigen::VectorXd probe(dims);
448 for (uint32_t d = 0; d < dims; ++d) {
449 probe(d) = static_cast<double>(base_cell[d] + offsets[d]) + 0.5;
450 }
451 check_cell(detail::hash_cell_nd(probe, m_inv_cell));
452 } while (advance());
453 }
454}
455
456// =========================================================================
457// Brute-force query
458// =========================================================================
459
460template <typename PointT>
462 const SpatialSnapshot<PointT>& snap,
463 const PointT& center,
464 float radius_sq,
465 std::vector<QueryResult>& out) const
466{
467 for (uint32_t i = 0; i < static_cast<uint32_t>(snap.positions.size()); ++i) {
468 float d_sq = m_distance_fn(center, snap.positions[i]);
469 if (d_sq <= radius_sq) {
470 out.push_back({ snap.slot_to_id[i], d_sq });
471 }
472 }
473}
474
475// =========================================================================
476// macOS hazard pointer protocol
477// =========================================================================
478
479#ifdef MAYAFLUX_PLATFORM_MACOS
480
481template <typename PointT>
482std::pair<const SpatialSnapshot<PointT>*, size_t>
484{
485 size_t slot = m_hazard_counter.fetch_add(1, std::memory_order_relaxed) % MAX_READERS;
486 const SpatialSnapshot<PointT>* current;
487 do {
488 current = m_snapshot.load(std::memory_order_acquire);
489 m_hazard_ptrs[slot].store(current, std::memory_order_release);
490 } while (current != m_snapshot.load(std::memory_order_acquire));
491 return { current, slot };
492}
493
494template <typename PointT>
495void SpatialIndex<PointT>::release_snapshot(size_t slot) const
496{
497 m_hazard_ptrs[slot].store(nullptr, std::memory_order_release);
498}
499
500template <typename PointT>
501void SpatialIndex<PointT>::retire_snapshot(const SpatialSnapshot<PointT>* old)
502{
503 m_retired.push_back(old);
504
505 auto it = m_retired.begin();
506 while (it != m_retired.end()) {
507 bool referenced = false;
508 for (size_t i = 0; i < MAX_READERS; ++i) {
509 if (m_hazard_ptrs[i].load(std::memory_order_acquire) == *it) {
510 referenced = true;
511 break;
512 }
513 }
514 if (!referenced) {
515 delete *it;
516 it = m_retired.erase(it);
517 } else {
518 ++it;
519 }
520 }
521}
522
523#endif // MAYAFLUX_PLATFORM_MACOS
524
525// =========================================================================
526// Factory functions
527// =========================================================================
528
529std::unique_ptr<SpatialIndex3D> make_spatial_index_3d(float cell_size)
530{
531 return std::make_unique<SpatialIndex3D>(
532 cell_size,
533 [](const glm::vec3& a, const glm::vec3& b) -> float {
534 glm::vec3 d = a - b;
535 return glm::dot(d, d);
536 });
537}
538
539std::unique_ptr<SpatialIndexND> make_spatial_index_nd(float cell_size, uint32_t dimensions)
540{
541 auto idx = std::make_unique<SpatialIndexND>(
542 cell_size,
543 [](const Eigen::VectorXd& a, const Eigen::VectorXd& b) -> float {
544 return static_cast<float>((a - b).squaredNorm());
545 });
546
547 if (dimensions > detail::MAX_GRID_DIMENSIONS) {
549 "SpatialIndexND: {} dimensions exceeds grid threshold ({}), using brute-force",
550 dimensions, detail::MAX_GRID_DIMENSIONS);
551 }
552
553 return idx;
554}
555
556// =========================================================================
557// Explicit template instantiations
558// =========================================================================
559
560template class SpatialIndex<glm::vec3>;
561template class SpatialIndex<Eigen::VectorXd>;
562
563} // namespace MayaFlux::Kinesis
#define MF_INFO(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
uint32_t h
Definition InkPress.cpp:25
size_t a
size_t b
Range radius
glm::vec3 position
uint32_t id
void query_grid(const SpatialSnapshot< PointT > &snap, const PointT &center, float radius_sq, std::vector< QueryResult > &out) const
uint32_t insert(const PointT &position)
Insert a new entity at the given position.
void clear()
Remove all entities from the write side.
void query_brute(const SpatialSnapshot< PointT > &snap, const PointT &center, float radius_sq, std::vector< QueryResult > &out) const
void publish()
Atomically publish the current write-side state as a new read snapshot.
std::vector< QueryResult > k_nearest(const PointT &center, uint32_t k) const
Find the k nearest entities to a point.
std::optional< PointT > position_of(uint32_t id) const
Read the position of an entity from the published snapshot.
uint64_t hash_cell(const PointT &p) const
std::vector< std::pair< uint32_t, PointT > > all() const
Return all entity ids and positions from the published snapshot.
void rebuild_grid(SpatialSnapshot< PointT > &snap) const
SpatialIndex(float cell_size, DistanceFn distance)
Construct a spatial index.
void update(uint32_t id, const PointT &position)
Move an existing entity to a new position.
size_t count() const
Entity count in the published snapshot.
std::vector< QueryResult > within_radius(const PointT &center, float radius) const
Find all entities within a radius of a point.
std::function< float(const PointT &, const PointT &)> DistanceFn
void remove(uint32_t id)
Remove an entity from the index.
Lock-free spatial acceleration structure with atomic snapshot publication.
@ Runtime
General runtime operations (default fallback)
@ Kinesis
General mathematical and physics algorithns.
uint32_t get_dimensions(const PointT &p)
constexpr uint32_t MAX_GRID_DIMENSIONS
uint64_t hash_cell_3d(int32_t cx, int32_t cy, int32_t cz)
uint64_t hash_cell_nd(const Eigen::VectorXd &p, float inv_cell)
std::array< int32_t, 3 > cell_coords_3d(const glm::vec3 &p, float inv_cell)
std::unique_ptr< SpatialIndexND > make_spatial_index_nd(float cell_size, uint32_t dimensions)
Create an N-dimensional spatial index with Euclidean squared distance.
SpatialField distance(const glm::vec3 &anchor, float radius, DistanceMetric metric=DistanceMetric::EUCLIDEAN)
Normalized distance from an anchor point using the specified metric.
std::unique_ptr< SpatialIndex3D > make_spatial_index_3d(float cell_size)
Create a 3D spatial index with Euclidean squared distance.
Result entry from a spatial query, carrying entity id and squared distance.
std::vector< PointT > positions
Packed position storage indexed by internal slot.
uint32_t dimensions
Dimensionality (3 for glm::vec3, runtime for Eigen).
std::vector< uint32_t > slot_to_id
Maps internal slot index back to external entity id.
std::unordered_map< uint64_t, std::vector< uint32_t > > cells
Grid cell contents. Key is hashed cell coordinate.
Immutable spatial snapshot published atomically for lock-free reads.
static uint32_t dimensions(const Eigen::VectorXd &p)
static float sq_distance_euclidean(const Eigen::VectorXd &a, const Eigen::VectorXd &b)
static float sq_distance_euclidean(const glm::vec3 &a, const glm::vec3 &b)