Skip to content

Commit 53374a2

Browse files
RubberLandingsvchb
andauthored
Spatial Hashing (#101)
* Added test file for Spatial Hashing. Updated the module file to expose SpatialHashingCellList to the users. * Minor changes. * Spatial Hashing works with initialize!() and empty!(). * Minor changes. * Working version for collision handling. * Simple test for collisions with spatial hashing and foreach_neighbor(). * Add update!() for neighborhood_search tests. * Running version for collision handling. Implement changes in spatial_hashing.jl to correct collision handling. Push collision check in __foreach_neighbor() for better performance. Extend test to test on multiple list sizes. Change plot.jl to test SpatialHashingCellList against the other data structures. * Fixed collision handling with empty cells. Clean up the code. * Merge different tests for spatial hashing together. * Clean up. * Add documentation. * Clean up. * Update test for spatial hashing, add test for empty list. * Add copy_cell_list() for spatial hashing. Include SpatialHashingCellList in test/neighborhood_search.jl. * Update check_collision(), the tests are succeeding. Add check_cell_collision() for benchmarking. * Clean up. * Clean up. * Fix code in foreach_neighbor after merge. * Resolve requested changes: - restore benchmark plotting code - remove NDIMS as a field for SpatialHashingCellList - change evaluation order of cell_collision in foreach_neighbor() to increase efficiency - add small, explicit test for cell collision - clean up * Resolve requested changes: - add comments - adjust version in Project.toml * Minor changes. * Resolve requested changes. * Resolve requested change for docs for SpatialHashingCellList. * Change citation of Ihmsen in doc for SpatialHashingCellList. * Fix formatting. * Fix formatting. --------- Co-authored-by: Sven Berger <[email protected]>
1 parent d4944f5 commit 53374a2

File tree

7 files changed

+249
-10
lines changed

7 files changed

+249
-10
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@ LinearAlgebra = "1"
2222
Polyester = "0.7.5"
2323
Reexport = "1"
2424
StaticArrays = "1"
25-
julia = "1.10"
25+
julia = "1.10"

src/PointNeighbors.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ include("gpu.jl")
2222

2323
export foreach_point_neighbor, foreach_neighbor
2424
export TrivialNeighborhoodSearch, GridNeighborhoodSearch, PrecomputedNeighborhoodSearch
25-
export DictionaryCellList, FullGridCellList
25+
export DictionaryCellList, FullGridCellList, SpatialHashingCellList
2626
export ParallelUpdate, SemiParallelUpdate, SerialIncrementalUpdate, SerialUpdate,
2727
ParallelIncrementalUpdate
2828
export requires_update

src/cell_lists/cell_lists.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ abstract type AbstractCellList end
66

77
include("dictionary.jl")
88
include("full_grid.jl")
9+
include("spatial_hashing.jl")

src/cell_lists/spatial_hashing.jl

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""
2+
SpatialHashingCellList{NDIMS}(; list_size)
3+
4+
A basic spatial hashing implementation. Similar to [`DictionaryCellList`](@ref), the domain is discretized into cells,
5+
and the particles in each cell are stored in a hash map. The hash is computed using the spatial location of each cell,
6+
as described by Ihmsen et al. (2011)(@cite Ihmsen2011). By using a hash map that stores entries only for non-empty cells,
7+
the domain is effectively infinite. The size of the hash map is recommended to be approximately twice the number of particles
8+
to balance memory consumption against the likelihood of hash collisions.
9+
10+
# Arguments
11+
- `NDIMS::Int`: Number of spatial dimensions (e.g., `2` or `3`).
12+
- `list_size::Int`: Size of the hash map (e.g., `2 * n_points`) .
13+
"""
14+
15+
struct SpatialHashingCellList{NDIMS, CL, CI, CF} <: AbstractCellList
16+
points :: CL
17+
coords :: CI
18+
collisions :: CF
19+
list_size :: Int
20+
end
21+
22+
@inline index_type(::SpatialHashingCellList) = Int32
23+
24+
@inline Base.ndims(::SpatialHashingCellList{NDIMS}) where {NDIMS} = NDIMS
25+
26+
function supported_update_strategies(::SpatialHashingCellList)
27+
return (SerialUpdate,)
28+
end
29+
30+
function SpatialHashingCellList{NDIMS}(list_size) where {NDIMS}
31+
points = [Int[] for _ in 1:list_size]
32+
collisions = [false for _ in 1:list_size]
33+
coords = [ntuple(_ -> typemin(Int), NDIMS) for _ in 1:list_size]
34+
return SpatialHashingCellList{NDIMS, typeof(points), typeof(coords),
35+
typeof(collisions)}(points, coords, collisions, list_size)
36+
end
37+
38+
function Base.empty!(cell_list::SpatialHashingCellList)
39+
(; list_size) = cell_list
40+
NDIMS = ndims(cell_list)
41+
42+
Base.empty!.(cell_list.points)
43+
cell_list.coords .= [ntuple(_ -> typemin(Int), NDIMS) for _ in 1:list_size]
44+
cell_list.collisions .= false
45+
return cell_list
46+
end
47+
48+
# For each entry in the hash table, store the coordinates of the cell of the first point being inserted at this entry.
49+
# If a point with a different cell coordinate is being added, we have found a collision.
50+
function push_cell!(cell_list::SpatialHashingCellList, cell, point)
51+
(; points, coords, collisions, list_size) = cell_list
52+
NDIMS = ndims(cell_list)
53+
hash_key = spatial_hash(cell, list_size)
54+
push!(points[hash_key], point)
55+
56+
cell_coord = coords[hash_key]
57+
if cell_coord == ntuple(_ -> typemin(Int), NDIMS)
58+
# If this cell is not used yet, set cell coordinates
59+
coords[hash_key] = cell
60+
elseif cell_coord != cell
61+
# If it is already used by a different cell, mark as collision
62+
collisions[hash_key] = true
63+
end
64+
end
65+
66+
function deleteat_cell!(cell_list::SpatialHashingCellList, cell, i)
67+
deleteat!(cell_list[cell], i)
68+
end
69+
70+
@inline each_cell_index(cell_list::SpatialHashingCellList) = eachindex(cell_list.points)
71+
72+
function copy_cell_list(cell_list::SpatialHashingCellList, search_radius,
73+
periodic_box)
74+
(; list_size) = cell_list
75+
NDIMS = ndims(cell_list)
76+
77+
return SpatialHashingCellList{NDIMS}(list_size)
78+
end
79+
80+
@inline function Base.getindex(cell_list::SpatialHashingCellList, cell::Tuple)
81+
return cell_list.points[spatial_hash(cell, length(cell_list.points))]
82+
end
83+
84+
@inline function Base.getindex(cell_list::SpatialHashingCellList, i::Integer)
85+
return cell_list.points[i]
86+
end
87+
88+
@inline function is_correct_cell(cell_list::SpatialHashingCellList{<:Any, Nothing},
89+
coords, cell_index::Array)
90+
return coords == cell_index
91+
end
92+
93+
# Hash functions according to Ihmsen et al. (2001)
94+
function spatial_hash(cell::NTuple{1, Real}, list_size)
95+
return mod(cell[1] * 73856093, list_size) + 1
96+
end
97+
98+
function spatial_hash(cell::NTuple{2, Real}, list_size)
99+
i, j = cell
100+
101+
return mod(xor(i * 73856093, j * 19349663), list_size) + 1
102+
end
103+
104+
function spatial_hash(cell::NTuple{3, Real}, list_size)
105+
i, j, k = cell
106+
107+
return mod(xor(i * 73856093, j * 19349663, k * 83492791), list_size) + 1
108+
end

src/nhs_grid.jl

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,19 +449,58 @@ function update_grid!(neighborhood_search::Union{GridNeighborhoodSearch{<:Any,
449449
initialize_grid!(neighborhood_search, y; parallelization_backend, eachindex_y)
450450
end
451451

452+
function check_collision(neighbor_cell_, neighbor_coords, cell_list, nhs)
453+
# This is only relevant for the `SpatialHashingCellList`
454+
return false
455+
end
456+
457+
# Check if `neighbor_coords` belong to `neighbor_cell`, which might not be the case
458+
# with the `SpatialHashingCellList` if this cell has a collision.
459+
function check_collision(neighbor_cell_::CartesianIndex, neighbor_coords,
460+
cell_list::SpatialHashingCellList, nhs)
461+
(; list_size, collisions, coords) = cell_list
462+
neighbor_cell = periodic_cell_index(Tuple(neighbor_cell_), nhs)
463+
464+
return neighbor_cell != cell_coords(neighbor_coords, nhs)
465+
end
466+
467+
function check_cell_collision(neighbor_cell_::CartesianIndex,
468+
cell_list, nhs)
469+
# This is only relevant for the `SpatialHashingCellList`
470+
return false
471+
end
472+
473+
# Check if there is a collision in this cell, meaning there is at least one point
474+
# in this list that doesn't actually belong in this cell.
475+
function check_cell_collision(neighbor_cell_::CartesianIndex,
476+
cell_list::SpatialHashingCellList, nhs)
477+
(; list_size, collisions, coords) = cell_list
478+
neighbor_cell = periodic_cell_index(Tuple(neighbor_cell_), nhs)
479+
hash = spatial_hash(neighbor_cell, list_size)
480+
481+
# `collisions[hash] == true` means points from multiple cells are in this list.
482+
# `collisions[hash] == false` means points from only one cells are in this list.
483+
# We could still have a collision though, if this one cell is not `neighbor_cell`,
484+
# which is possible when `neighbor_cell` is empty.
485+
return collisions[hash] || coords[hash] != neighbor_cell
486+
end
487+
452488
# Specialized version of the function in `neighborhood_search.jl`, which is faster
453489
# than looping over `eachneighbor`.
454490
@inline function foreach_neighbor(f, neighbor_system_coords,
455491
neighborhood_search::GridNeighborhoodSearch,
456492
point, point_coords, search_radius)
457-
(; periodic_box) = neighborhood_search
458-
493+
(; cell_list, periodic_box) = neighborhood_search
459494
cell = cell_coords(point_coords, neighborhood_search)
460495

461496
for neighbor_cell_ in neighboring_cells(cell, neighborhood_search)
462497
neighbor_cell = Tuple(neighbor_cell_)
463498
neighbors = points_in_cell(neighbor_cell, neighborhood_search)
464499

500+
# Boolean to indicate if this cell has a collision (only with `SpatialHashingCellList`)
501+
cell_collision = check_cell_collision(neighbor_cell_,
502+
cell_list, neighborhood_search)
503+
465504
for neighbor_ in eachindex(neighbors)
466505
neighbor = @inbounds neighbors[neighbor_]
467506

@@ -480,6 +519,14 @@ end
480519
if distance2 <= search_radius^2
481520
distance = sqrt(distance2)
482521

522+
# If this cell has a collision, check if this point belongs to this cell
523+
# (only with `SpatialHashingCellList`).
524+
if cell_collision &&
525+
check_collision(neighbor_cell_, neighbor_coords, cell_list,
526+
neighborhood_search)
527+
continue
528+
end
529+
483530
# Inline to avoid loss of performance
484531
# compared to not using `foreach_point_neighbor`.
485532
@inline f(point, neighbor, pos_diff, distance)

test/cell_lists/spatial_hashing.jl

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
@testset verbose=true "SpatialHashingCellList" begin
2+
@testset "Collision Handling With Empty Cells" begin
3+
# The point is in cell (-1, 0) which has a hash collision with cell (-2, -1)
4+
coordinates = [-0.05; 0.05;;]
5+
NDIMS = size(coordinates, 1)
6+
n_points = size(coordinates, 2)
7+
search_radius = 0.1 + 10 * eps()
8+
point_index = 1
9+
10+
nhs = GridNeighborhoodSearch{2}(; search_radius, n_points,
11+
cell_list = SpatialHashingCellList{NDIMS}(n_points))
12+
initialize_grid!(nhs, coordinates)
13+
14+
@testset "Test For Collision" begin
15+
cell1 = (-1, 0)
16+
cell2 = (-2, -1)
17+
cell1_hash = PointNeighbors.spatial_hash(cell1, n_points)
18+
cell2_hash = PointNeighbors.spatial_hash(cell2, n_points)
19+
points1 = nhs.cell_list[cell1]
20+
points2 = nhs.cell_list[cell2]
21+
22+
@test points1 == points2 == [1]
23+
@test cell1_hash == cell2_hash
24+
end
25+
26+
neighbors = Int[]
27+
foreach_neighbor(coordinates, coordinates, nhs,
28+
point_index) do point, neighbor, pos_diff, distance
29+
push!(neighbors, neighbor)
30+
end
31+
32+
@test neighbors == [1]
33+
end
34+
35+
@testset "Collision Handling With Non-Empty Cells" begin
36+
# Cell (-1, 0) with point 1 has a hash collision with cell (-2, -1) with point 2
37+
coordinates = [[-0.05 -0.15]; [0.05 -0.05]]
38+
NDIMS = size(coordinates, 1)
39+
n_points = size(coordinates, 2)
40+
search_radius = 0.1 + 10 * eps()
41+
point_index = 1
42+
43+
nhs = GridNeighborhoodSearch{2}(; search_radius, n_points,
44+
cell_list = SpatialHashingCellList{NDIMS}(n_points))
45+
initialize_grid!(nhs, coordinates)
46+
47+
@testset "Test For Collision" begin
48+
cell1 = (-1, 0)
49+
cell2 = (-2, -1)
50+
cell1_hash = PointNeighbors.spatial_hash(cell1, n_points)
51+
cell2_hash = PointNeighbors.spatial_hash(cell2, n_points)
52+
points1 = nhs.cell_list[cell1]
53+
points2 = nhs.cell_list[cell2]
54+
55+
@test points1 == points2 == [1, 2]
56+
@test cell1_hash == cell2_hash
57+
end
58+
59+
neighbors = [Int[] for _ in axes(coordinates, 2)]
60+
foreach_point_neighbor(coordinates, coordinates, nhs,
61+
points = axes(coordinates, 2)) do point, neighbor, pos_diff,
62+
distance
63+
push!(neighbors[point], neighbor)
64+
end
65+
66+
@test neighbors[1] == [1]
67+
@test neighbors[2] == [2]
68+
end
69+
end

test/neighborhood_search.jl

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,20 @@
5858
search_radius,
5959
backend = Vector{Vector{Int32}})),
6060
PrecomputedNeighborhoodSearch{NDIMS}(; search_radius, n_points,
61-
periodic_box = periodic_boxes[i])
61+
periodic_box = periodic_boxes[i]),
62+
GridNeighborhoodSearch{NDIMS}(; search_radius, n_points,
63+
periodic_box = periodic_boxes[i],
64+
cell_list = SpatialHashingCellList{NDIMS}(2 *
65+
n_points))
6266
]
6367

6468
names = [
6569
"`TrivialNeighborhoodSearch`",
6670
"`GridNeighborhoodSearch`",
6771
"`GridNeighborhoodSearch` with `FullGridCellList` with `DynamicVectorOfVectors`",
6872
"`GridNeighborhoodSearch` with `FullGridCellList` with `Vector{Vector}`",
69-
"`PrecomputedNeighborhoodSearch`"
73+
"`PrecomputedNeighborhoodSearch`",
74+
"`GridNeighborhoodSearch` with `SpatialHashingCellList`"
7075
]
7176

7277
# Also test copied templates
@@ -80,7 +85,10 @@
8085
cell_list = FullGridCellList(min_corner = periodic_boxes[i].min_corner,
8186
max_corner = periodic_boxes[i].max_corner,
8287
backend = Vector{Vector{Int32}})),
83-
PrecomputedNeighborhoodSearch{NDIMS}(periodic_box = periodic_boxes[i])
88+
PrecomputedNeighborhoodSearch{NDIMS}(periodic_box = periodic_boxes[i]),
89+
GridNeighborhoodSearch{NDIMS}(periodic_box = periodic_boxes[i],
90+
cell_list = SpatialHashingCellList{NDIMS}(2 *
91+
n_points))
8492
]
8593
copied_nhs = copy_neighborhood_search.(template_nhs, search_radius, n_points)
8694
append!(neighborhood_searches, copied_nhs)
@@ -184,7 +192,10 @@
184192
max_corner,
185193
search_radius,
186194
backend = Vector{Vector{Int}})),
187-
PrecomputedNeighborhoodSearch{NDIMS}(; search_radius, n_points)
195+
PrecomputedNeighborhoodSearch{NDIMS}(; search_radius, n_points),
196+
GridNeighborhoodSearch{NDIMS}(; search_radius, n_points,
197+
cell_list = SpatialHashingCellList{NDIMS}(2 *
198+
n_points))
188199
]
189200

190201
names = [
@@ -195,7 +206,8 @@
195206
"`GridNeighborhoodSearch` with `FullGridCellList` with `DynamicVectorOfVectors` and `ParallelIncrementalUpdate`",
196207
"`GridNeighborhoodSearch` with `FullGridCellList` with `DynamicVectorOfVectors` and `SemiParallelUpdate`",
197208
"`GridNeighborhoodSearch` with `FullGridCellList` with `Vector{Vector}`",
198-
"`PrecomputedNeighborhoodSearch`"
209+
"`PrecomputedNeighborhoodSearch`",
210+
"`GridNeighborhoodSearch` with `SpatialHashingCellList`"
199211
]
200212

201213
# Also test copied templates
@@ -214,7 +226,9 @@
214226
GridNeighborhoodSearch{NDIMS}(cell_list = FullGridCellList(; min_corner,
215227
max_corner,
216228
backend = Vector{Vector{Int32}})),
217-
PrecomputedNeighborhoodSearch{NDIMS}()
229+
PrecomputedNeighborhoodSearch{NDIMS}(),
230+
GridNeighborhoodSearch{NDIMS}(cell_list = SpatialHashingCellList{NDIMS}(2 *
231+
n_points))
218232
]
219233
copied_nhs = copy_neighborhood_search.(template_nhs, search_radius, n_points)
220234
append!(neighborhood_searches, copied_nhs)

0 commit comments

Comments
 (0)