15 template <
typename Po
intT>
20 static constexpr uint32_t dimensions = 3;
21 static constexpr bool fixed_dimension =
true;
26 return glm::dot(d, d);
32 static constexpr bool fixed_dimension =
false;
36 return static_cast<float>((
a -
b).squaredNorm());
41 return static_cast<uint32_t
>(p.size());
45 template <
typename Po
intT>
63template <
typename Po
intT>
65 : m_cell_size(cell_size)
66 , m_inv_cell(1.0F / cell_size)
68 , m_use_grid(static_cast<bool>(detail::PointTraits<PointT>::fixed_dimension))
71#ifdef MAYAFLUX_PLATFORM_MACOS
72 for (
auto& hp : m_hazard_ptrs) {
78template <
typename Po
intT>
81#ifdef MAYAFLUX_PLATFORM_MACOS
82 auto* snap = m_snapshot.load(std::memory_order_acquire);
84 for (
auto* r : m_retired) {
94template <
typename Po
intT>
97 uint32_t
id = m_next_id++;
100 if (!m_free_slots.empty()) {
101 slot = m_free_slots.back();
102 m_free_slots.pop_back();
104 m_slot_to_id[slot] =
id;
106 slot =
static_cast<uint32_t
>(m_positions.size());
108 m_slot_to_id.push_back(
id);
111 m_id_to_slot[
id] = slot;
114 if (m_positions.size() == 1) {
123template <
typename Po
intT>
126 auto it = m_id_to_slot.find(
id);
127 if (it == m_id_to_slot.end()) {
129 "SpatialIndex::update: unknown id {}",
id);
135template <
typename Po
intT>
138 auto it = m_id_to_slot.find(
id);
139 if (it == m_id_to_slot.end()) {
141 "SpatialIndex::remove: unknown id {}",
id);
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);
151template <
typename Po
intT>
155 m_id_to_slot.clear();
156 m_slot_to_id.clear();
157 m_free_slots.clear();
164template <
typename Po
intT>
167 auto snap = std::make_unique<SpatialSnapshot<PointT>>();
168 snap->cell_size = m_cell_size;
173 snap->dimensions = m_positions.empty()
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);
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;
194#ifdef MAYAFLUX_PLATFORM_MACOS
195 auto* old = m_snapshot.exchange(snap.release(), std::memory_order_acq_rel);
197 retire_snapshot(old);
202 std::memory_order_release);
210template <
typename Po
intT>
214 for (uint32_t i = 0; i < static_cast<uint32_t>(snap.
positions.size()); ++i) {
216 snap.
cells[
h].push_back(i);
220template <
typename Po
intT>
223 if constexpr (std::is_same_v<PointT, glm::vec3>) {
235template <
typename Po
intT>
237 const PointT& center,
240 std::vector<QueryResult> results;
243#ifdef MAYAFLUX_PLATFORM_MACOS
244 auto [snap, slot] = acquire_snapshot();
249 auto snap_ptr = m_snapshot.load(std::memory_order_acquire);
253 const auto* snap = snap_ptr.get();
257 query_grid(*snap, center, radius_sq, results);
259 query_brute(*snap, center, radius_sq, results);
262#ifdef MAYAFLUX_PLATFORM_MACOS
263 release_snapshot(slot);
269template <
typename Po
intT>
271 const PointT& center,
274#ifdef MAYAFLUX_PLATFORM_MACOS
275 auto [snap, slot] = acquire_snapshot();
276 if (!snap || snap->positions.empty()) {
278 release_snapshot(slot);
282 auto snap_ptr = m_snapshot.load(std::memory_order_acquire);
283 if (!snap_ptr || snap_ptr->positions.empty()) {
286 const auto* snap = snap_ptr.get();
289 k = std::min(k,
static_cast<uint32_t
>(snap->positions.size()));
292 return a.distance_sq <
b.distance_sq;
294 std::priority_queue<QueryResult, std::vector<QueryResult>,
decltype(cmp)> heap(cmp);
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) {
302 heap.push({ snap->slot_to_id[i], d_sq });
306 std::vector<QueryResult> results;
307 results.reserve(heap.size());
308 while (!heap.empty()) {
309 results.push_back(heap.top());
312 std::ranges::reverse(results);
314#ifdef MAYAFLUX_PLATFORM_MACOS
315 release_snapshot(slot);
321template <
typename Po
intT>
324#ifdef MAYAFLUX_PLATFORM_MACOS
325 auto [snap, slot] = acquire_snapshot();
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])
333 release_snapshot(slot);
336 auto snap_ptr = m_snapshot.load(std::memory_order_acquire);
340 auto it = snap_ptr->id_to_slot.find(
id);
341 if (it == snap_ptr->id_to_slot.end()) {
344 return snap_ptr->positions[it->second];
348template <
typename Po
intT>
351#ifdef MAYAFLUX_PLATFORM_MACOS
352 auto [snap, slot] = acquire_snapshot();
353 size_t n = snap ? snap->positions.size() : 0;
355 release_snapshot(slot);
358 auto snap_ptr = m_snapshot.load(std::memory_order_acquire);
359 return snap_ptr ? snap_ptr->positions.size() : 0;
363template <
typename Po
intT>
366 std::vector<std::pair<uint32_t, PointT>> result;
368#ifdef MAYAFLUX_PLATFORM_MACOS
369 auto [snap, slot] = acquire_snapshot();
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]);
377 release_snapshot(slot);
379 auto snap_ptr = m_snapshot.load(std::memory_order_acquire);
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]);
396template <
typename Po
intT>
399 const PointT& center,
401 std::vector<QueryResult>& out)
const
403 auto extent =
static_cast<int32_t
>(std::ceil(std::sqrt(radius_sq) * m_inv_cell));
405 auto check_cell = [&](uint64_t
h) {
406 auto it = snap.
cells.find(
h);
407 if (it == snap.
cells.end()) {
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 });
418 if constexpr (std::is_same_v<PointT, glm::vec3>) {
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) {
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));
434 std::vector<int32_t> offsets(dims, -extent);
436 auto advance = [&]() ->
bool {
437 for (uint32_t d = 0; d < dims; ++d) {
438 if (++offsets[d] <= extent) {
441 offsets[d] = -extent;
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;
460template <
typename Po
intT>
463 const PointT& center,
465 std::vector<QueryResult>& out)
const
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) {
479#ifdef MAYAFLUX_PLATFORM_MACOS
481template <
typename Po
intT>
482std::pair<const SpatialSnapshot<PointT>*,
size_t>
485 size_t slot = m_hazard_counter.fetch_add(1, std::memory_order_relaxed) % MAX_READERS;
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 };
494template <
typename Po
intT>
495void SpatialIndex<PointT>::release_snapshot(
size_t slot)
const
497 m_hazard_ptrs[slot].store(
nullptr, std::memory_order_release);
500template <
typename Po
intT>
501void SpatialIndex<PointT>::retire_snapshot(
const SpatialSnapshot<PointT>* old)
503 m_retired.push_back(old);
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) {
516 it = m_retired.erase(it);
531 return std::make_unique<SpatialIndex3D>(
533 [](
const glm::vec3&
a,
const glm::vec3&
b) ->
float {
535 return glm::dot(d, d);
541 auto idx = std::make_unique<SpatialIndexND>(
543 [](
const Eigen::VectorXd&
a,
const Eigen::VectorXd&
b) ->
float {
544 return static_cast<float>((
a -
b).squaredNorm());
549 "SpatialIndexND: {} dimensions exceeds grid threshold ({}), using brute-force",
560template class SpatialIndex<glm::vec3>;
561template class SpatialIndex<Eigen::VectorXd>;
#define MF_INFO(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
void query_grid(const SpatialSnapshot< PointT > &snap, const PointT ¢er, 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 ¢er, 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 ¢er, 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 ¢er, 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)