From 63546450116e7646290bb83dcdaffb521c263e45 Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Tue, 6 May 2025 15:25:08 +0800 Subject: [PATCH 01/21] Add support for different backends in SpatialHashingCellList. --- src/cell_lists/full_grid.jl | 17 ++++++---- src/cell_lists/spatial_hashing.jl | 54 ++++++++++++++++++++++++------- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/src/cell_lists/full_grid.jl b/src/cell_lists/full_grid.jl index 169c6819..8d3a7d19 100644 --- a/src/cell_lists/full_grid.jl +++ b/src/cell_lists/full_grid.jl @@ -59,23 +59,26 @@ function FullGridCellList(; min_corner, max_corner, if search_radius < eps() # Create an empty "template" cell list to be used with `copy_cell_list` - cells = construct_backend(backend, 0, max_points_per_cell) + cells = construct_backend(FullGridCellList, backend, 0, max_points_per_cell) linear_indices = LinearIndices(ntuple(_ -> 0, length(min_corner))) else n_cells_per_dimension = ceil.(Int, (max_corner .- min_corner) ./ search_radius) linear_indices = LinearIndices(Tuple(n_cells_per_dimension)) - cells = construct_backend(backend, n_cells_per_dimension, max_points_per_cell) + cells = construct_backend(FullGridCellList, backend, n_cells_per_dimension, + max_points_per_cell) end return FullGridCellList(cells, linear_indices, min_corner, max_corner) end -function construct_backend(::Type{Vector{Vector{T}}}, size, max_points_per_cell) where {T} +function construct_backend(::Type{FullGridCellList}, ::Type{Vector{Vector{T}}}, size, + max_points_per_cell) where {T} return [T[] for _ in 1:prod(size)] end -function construct_backend(::Type{DynamicVectorOfVectors{T}}, size, +function construct_backend(::Type{FullGridCellList}, ::Type{DynamicVectorOfVectors{T}}, + size, max_points_per_cell) where {T} cells = DynamicVectorOfVectors{T}(max_outer_length = prod(size), max_inner_length = max_points_per_cell) @@ -88,9 +91,11 @@ end # `DynamicVectorOfVectors{T}`, but a type `DynamicVectorOfVectors{T1, T2, T3, T4}`. # While `A{T} <: A{T1, T2}`, this doesn't hold for the types. # `Type{A{T}} <: Type{A{T1, T2}}` is NOT true. -function construct_backend(::Type{DynamicVectorOfVectors{T1, T2, T3, T4}}, size, +function construct_backend(cell_list::Type{<:AbstractCellList}, + ::Type{DynamicVectorOfVectors{T1, T2, T3, T4}}, size, max_points_per_cell) where {T1, T2, T3, T4} - return construct_backend(DynamicVectorOfVectors{T1}, size, max_points_per_cell) + return construct_backend(cell_list, DynamicVectorOfVectors{T1}, size, + max_points_per_cell) end @inline function cell_coords(coords, periodic_box::Nothing, cell_list::FullGridCellList, diff --git a/src/cell_lists/spatial_hashing.jl b/src/cell_lists/spatial_hashing.jl index 4822cb2c..912750fd 100644 --- a/src/cell_lists/spatial_hashing.jl +++ b/src/cell_lists/spatial_hashing.jl @@ -13,7 +13,7 @@ to balance memory consumption against the likelihood of hash collisions. """ struct SpatialHashingCellList{NDIMS, CL, CI, CF} <: AbstractCellList - points :: CL + cells :: CL coords :: CI collisions :: CF list_size :: Int @@ -27,19 +27,43 @@ function supported_update_strategies(::SpatialHashingCellList) return (SerialUpdate,) end -function SpatialHashingCellList{NDIMS}(list_size) where {NDIMS} - points = [Int[] for _ in 1:list_size] +function SpatialHashingCellList{NDIMS}(list_size, + backend = DynamicVectorOfVectors{Int32}, + max_points_per_cell = 100) where {NDIMS} + cells = construct_backend(SpatialHashingCellList, backend, list_size, + max_points_per_cell) collisions = [false for _ in 1:list_size] coords = [ntuple(_ -> typemin(Int), NDIMS) for _ in 1:list_size] - return SpatialHashingCellList{NDIMS, typeof(points), typeof(coords), - typeof(collisions)}(points, coords, collisions, list_size) + + return SpatialHashingCellList{NDIMS, typeof(cells), typeof(coords), + typeof(collisions)}(cells, coords, collisions, list_size) +end + +function construct_backend(::Type{SpatialHashingCellList}, ::Type{Vector{Vector{T}}}, size, + max_points_per_cell) where {T} + return [T[] for _ in 1:size] +end + +function construct_backend(::Type{SpatialHashingCellList}, + ::Type{DynamicVectorOfVectors{T}}, size, + max_points_per_cell) where {T} + cells = DynamicVectorOfVectors{T}(max_outer_length = size, + max_inner_length = max_points_per_cell) + # Do I still need that resize? + resize!(cells, prod(size)) + + return cells end function Base.empty!(cell_list::SpatialHashingCellList) (; list_size) = cell_list NDIMS = ndims(cell_list) - Base.empty!.(cell_list.points) + # `Base.empty!.(cells)`, but for all backends + for i in eachindex(cell_list.cells) + emptyat!(cell_list.cells, i) + end + cell_list.coords .= [ntuple(_ -> typemin(Int), NDIMS) for _ in 1:list_size] cell_list.collisions .= false return cell_list @@ -48,10 +72,13 @@ end # For each entry in the hash table, store the coordinates of the cell of the first point being inserted at this entry. # If a point with a different cell coordinate is being added, we have found a collision. function push_cell!(cell_list::SpatialHashingCellList, cell, point) - (; points, coords, collisions, list_size) = cell_list + (; cells, coords, collisions, list_size) = cell_list NDIMS = ndims(cell_list) hash_key = spatial_hash(cell, list_size) - push!(points[hash_key], point) + + # Do I need that @boundscheck? + # @boundscheck check_cell_bounds(cell_list, cell) + @inbounds pushat!(cells, hash_key, point) cell_coord = coords[hash_key] if cell_coord == ntuple(_ -> typemin(Int), NDIMS) @@ -67,22 +94,25 @@ function deleteat_cell!(cell_list::SpatialHashingCellList, cell, i) deleteat!(cell_list[cell], i) end -@inline each_cell_index(cell_list::SpatialHashingCellList) = eachindex(cell_list.points) +@inline each_cell_index(cell_list::SpatialHashingCellList) = eachindex(cell_list.cells) function copy_cell_list(cell_list::SpatialHashingCellList, search_radius, periodic_box) (; list_size) = cell_list NDIMS = ndims(cell_list) - return SpatialHashingCellList{NDIMS}(list_size) + # Here I'm using max_points_per_cell which is defined in src/cell_lists/full_grid.jl + # Think about putting it somewhere all cell list can access it or copying it here + return SpatialHashingCellList{NDIMS}(list_size, typeof(cell_list.cells), + max_points_per_cell(cell_list.cells)) end @inline function Base.getindex(cell_list::SpatialHashingCellList, cell::Tuple) - return cell_list.points[spatial_hash(cell, length(cell_list.points))] + return cell_list.cells[spatial_hash(cell, length(cell_list.cells))] end @inline function Base.getindex(cell_list::SpatialHashingCellList, i::Integer) - return cell_list.points[i] + return cell_list.cells[i] end @inline function is_correct_cell(cell_list::SpatialHashingCellList{<:Any, Nothing}, From 6c70aed9a7124365880b28c0c877c38b1ed5c642 Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Thu, 8 May 2025 12:21:28 +0800 Subject: [PATCH 02/21] Minor change, remove unnecessary call to prod(). --- src/cell_lists/spatial_hashing.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cell_lists/spatial_hashing.jl b/src/cell_lists/spatial_hashing.jl index 912750fd..4e6c7459 100644 --- a/src/cell_lists/spatial_hashing.jl +++ b/src/cell_lists/spatial_hashing.jl @@ -49,8 +49,8 @@ function construct_backend(::Type{SpatialHashingCellList}, max_points_per_cell) where {T} cells = DynamicVectorOfVectors{T}(max_outer_length = size, max_inner_length = max_points_per_cell) - # Do I still need that resize? - resize!(cells, prod(size)) + # Do I still need that resize? + resize!(cells, size) return cells end From 8cb03ef4aaa38027d359394207cbaf159c93ca2a Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Tue, 13 May 2025 21:17:21 +0800 Subject: [PATCH 03/21] Add check_cell_bounds() for push_cell() for SHCL. Move check_cell_bounds() and construct_backends() to separate cell_lists_util.jl to share functionality between different cell lists. --- src/cell_lists/cell_lists.jl | 1 + src/cell_lists/cell_lists_util.jl | 39 +++++++++++++++++++++++++++++++ src/cell_lists/full_grid.jl | 32 ------------------------- src/cell_lists/spatial_hashing.jl | 20 ++-------------- 4 files changed, 42 insertions(+), 50 deletions(-) create mode 100644 src/cell_lists/cell_lists_util.jl diff --git a/src/cell_lists/cell_lists.jl b/src/cell_lists/cell_lists.jl index cde404ed..7c68760f 100644 --- a/src/cell_lists/cell_lists.jl +++ b/src/cell_lists/cell_lists.jl @@ -7,3 +7,4 @@ abstract type AbstractCellList end include("dictionary.jl") include("full_grid.jl") include("spatial_hashing.jl") +include("cell_lists_util.jl") diff --git a/src/cell_lists/cell_lists_util.jl b/src/cell_lists/cell_lists_util.jl new file mode 100644 index 00000000..d2aa45fc --- /dev/null +++ b/src/cell_lists/cell_lists_util.jl @@ -0,0 +1,39 @@ +@inline function check_cell_bounds(cell_list::AbstractCellList, cell::Integer) + (; cells) = cell_list + + checkbounds(cells, cell) +end + +# This became kinda unnecessary now since we mostly (only) call the function with the hash key (Integer) +# # Move that to spatial_hashing.jl +@inline function check_cell_bounds(cell_list::SpatialHashingCellList, cell::Tuple) + check_cell_bounds(cell_list, spatial_hash(cell, cell_list.list_size)) +end + +# We need the prod() because FullGridCellList's size is a tuple of cells per dimension whereas +# SpatialHashingCellList's size is an Integer for the number of cells in total. +function construct_backend(::Type{<:AbstractCellList}, ::Type{Vector{Vector{T}}}, size, + max_points_per_cell) where {T} + return [T[] for _ in 1:prod(size)] +end + +function construct_backend(::Type{<:AbstractCellList}, ::Type{DynamicVectorOfVectors{T}}, + size, + max_points_per_cell) where {T} + cells = DynamicVectorOfVectors{T}(max_outer_length = prod(size), + max_inner_length = max_points_per_cell) + resize!(cells, prod(size)) + + return cells +end + +# When `typeof(cell_list.cells)` is passed, we don't pass the type +# `DynamicVectorOfVectors{T}`, but a type `DynamicVectorOfVectors{T1, T2, T3, T4}`. +# While `A{T} <: A{T1, T2}`, this doesn't hold for the types. +# `Type{A{T}} <: Type{A{T1, T2}}` is NOT true. +function construct_backend(cell_list::Type{<:AbstractCellList}, + ::Type{DynamicVectorOfVectors{T1, T2, T3, T4}}, size, + max_points_per_cell) where {T1, T2, T3, T4} + return construct_backend(cell_list, DynamicVectorOfVectors{T1}, size, + max_points_per_cell) +end diff --git a/src/cell_lists/full_grid.jl b/src/cell_lists/full_grid.jl index 8d3a7d19..9620a69a 100644 --- a/src/cell_lists/full_grid.jl +++ b/src/cell_lists/full_grid.jl @@ -72,32 +72,6 @@ function FullGridCellList(; min_corner, max_corner, return FullGridCellList(cells, linear_indices, min_corner, max_corner) end -function construct_backend(::Type{FullGridCellList}, ::Type{Vector{Vector{T}}}, size, - max_points_per_cell) where {T} - return [T[] for _ in 1:prod(size)] -end - -function construct_backend(::Type{FullGridCellList}, ::Type{DynamicVectorOfVectors{T}}, - size, - max_points_per_cell) where {T} - cells = DynamicVectorOfVectors{T}(max_outer_length = prod(size), - max_inner_length = max_points_per_cell) - resize!(cells, prod(size)) - - return cells -end - -# When `typeof(cell_list.cells)` is passed, we don't pass the type -# `DynamicVectorOfVectors{T}`, but a type `DynamicVectorOfVectors{T1, T2, T3, T4}`. -# While `A{T} <: A{T1, T2}`, this doesn't hold for the types. -# `Type{A{T}} <: Type{A{T1, T2}}` is NOT true. -function construct_backend(cell_list::Type{<:AbstractCellList}, - ::Type{DynamicVectorOfVectors{T1, T2, T3, T4}}, size, - max_points_per_cell) where {T1, T2, T3, T4} - return construct_backend(cell_list, DynamicVectorOfVectors{T1}, size, - max_points_per_cell) -end - @inline function cell_coords(coords, periodic_box::Nothing, cell_list::FullGridCellList, cell_size) (; min_corner) = cell_list @@ -254,9 +228,3 @@ end error("particle coordinates are NaN or outside the domain bounds of the cell list") end end - -@inline function check_cell_bounds(cell_list::FullGridCellList, cell::Integer) - (; cells) = cell_list - - checkbounds(cells, cell) -end diff --git a/src/cell_lists/spatial_hashing.jl b/src/cell_lists/spatial_hashing.jl index 4e6c7459..28f77960 100644 --- a/src/cell_lists/spatial_hashing.jl +++ b/src/cell_lists/spatial_hashing.jl @@ -39,22 +39,6 @@ function SpatialHashingCellList{NDIMS}(list_size, typeof(collisions)}(cells, coords, collisions, list_size) end -function construct_backend(::Type{SpatialHashingCellList}, ::Type{Vector{Vector{T}}}, size, - max_points_per_cell) where {T} - return [T[] for _ in 1:size] -end - -function construct_backend(::Type{SpatialHashingCellList}, - ::Type{DynamicVectorOfVectors{T}}, size, - max_points_per_cell) where {T} - cells = DynamicVectorOfVectors{T}(max_outer_length = size, - max_inner_length = max_points_per_cell) - # Do I still need that resize? - resize!(cells, size) - - return cells -end - function Base.empty!(cell_list::SpatialHashingCellList) (; list_size) = cell_list NDIMS = ndims(cell_list) @@ -76,8 +60,8 @@ function push_cell!(cell_list::SpatialHashingCellList, cell, point) NDIMS = ndims(cell_list) hash_key = spatial_hash(cell, list_size) - # Do I need that @boundscheck? - # @boundscheck check_cell_bounds(cell_list, cell) + # Correct to use hash key? + @boundscheck check_cell_bounds(cell_list, hash_key) @inbounds pushat!(cells, hash_key, point) cell_coord = coords[hash_key] From 6035d1fb2539db12db4364c3f35ef2c377038bd9 Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Tue, 13 May 2025 21:19:31 +0800 Subject: [PATCH 04/21] Add constructor to SPCL to work with Adapt.jl --- src/cell_lists/spatial_hashing.jl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/cell_lists/spatial_hashing.jl b/src/cell_lists/spatial_hashing.jl index 28f77960..2eb9d62b 100644 --- a/src/cell_lists/spatial_hashing.jl +++ b/src/cell_lists/spatial_hashing.jl @@ -17,6 +17,14 @@ struct SpatialHashingCellList{NDIMS, CL, CI, CF} <: AbstractCellList coords :: CI collisions :: CF list_size :: Int + + # This constructor is necessary for Adapt.jl to work with this struct + function SpatialHashingCellList(cells, coords::AbstractVector{<:NTuple{NDIMS}}, + collisions, list_size) where {NDIMS} + return new{NDIMS, typeof(cells), + typeof(coords), typeof(collisions)}(cells, coords, + collisions, list_size) + end end @inline index_type(::SpatialHashingCellList) = Int32 @@ -35,8 +43,7 @@ function SpatialHashingCellList{NDIMS}(list_size, collisions = [false for _ in 1:list_size] coords = [ntuple(_ -> typemin(Int), NDIMS) for _ in 1:list_size] - return SpatialHashingCellList{NDIMS, typeof(cells), typeof(coords), - typeof(collisions)}(cells, coords, collisions, list_size) + return SpatialHashingCellList(cells, coords, collisions, list_size) end function Base.empty!(cell_list::SpatialHashingCellList) From c512782e0839f9abe4438e7671f2a469c663870b Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Wed, 14 May 2025 14:15:04 +0800 Subject: [PATCH 05/21] Minor changes. --- src/cell_lists/spatial_hashing.jl | 24 +++++++++++++++++++++++- src/nhs_grid.jl | 3 +++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/cell_lists/spatial_hashing.jl b/src/cell_lists/spatial_hashing.jl index 2eb9d62b..fbe73167 100644 --- a/src/cell_lists/spatial_hashing.jl +++ b/src/cell_lists/spatial_hashing.jl @@ -32,7 +32,7 @@ end @inline Base.ndims(::SpatialHashingCellList{NDIMS}) where {NDIMS} = NDIMS function supported_update_strategies(::SpatialHashingCellList) - return (SerialUpdate,) + return (SerialUpdate, ParallelUpdate) end function SpatialHashingCellList{NDIMS}(list_size, @@ -81,6 +81,28 @@ function push_cell!(cell_list::SpatialHashingCellList, cell, point) end end +function push_cell_atomic!(cell_list::SpatialHashingCellList, cell, point) + (; cells, coords, collisions, list_size) = cell_list + NDIMS = ndims(cell_list) + hash_key = spatial_hash(cell, list_size) + + # Correct to use hash key? + @boundscheck check_cell_bounds(cell_list, hash_key) + @inbounds pushat_atomic!(cells, hash_key, point) + + cell_coord = coords[hash_key] + if cell_coord == ntuple(_ -> typemin(Int), NDIMS) + # If this cell is not used yet, set cell coordinates + # Atomix.@atomic coords[hash_key] = cell + coords[hash_key] = cell + elseif cell_coord != cell + # If it is already used by a different cell, mark as collision + # Atomix.@atomic collisions[hash_key] = true + collisions[hash_key] = true + end +end + + function deleteat_cell!(cell_list::SpatialHashingCellList, cell, i) deleteat!(cell_list[cell], i) end diff --git a/src/nhs_grid.jl b/src/nhs_grid.jl index ba613ecd..b1af37de 100644 --- a/src/nhs_grid.jl +++ b/src/nhs_grid.jl @@ -378,6 +378,9 @@ end end # Fully parallel incremental update with atomic push. +# TODO `cell_list.cells.lengths` and `cell_list.cells.backend` are hardcoded +# for `FullGridCellList` and `SpatialHashingCellList`, which are currently +# the only implementations supporting this update strategy. function update_grid!(neighborhood_search::GridNeighborhoodSearch{<:Any, ParallelIncrementalUpdate}, y::AbstractMatrix; parallelization_backend = default_backend(y), From d5f29609f953607c892de6750a4bcd0c1beeedcb Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Wed, 14 May 2025 14:51:34 +0800 Subject: [PATCH 06/21] Add SHCL to gpu.jl --- src/cell_lists/spatial_hashing.jl | 1 - src/gpu.jl | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cell_lists/spatial_hashing.jl b/src/cell_lists/spatial_hashing.jl index fbe73167..44f21f7b 100644 --- a/src/cell_lists/spatial_hashing.jl +++ b/src/cell_lists/spatial_hashing.jl @@ -102,7 +102,6 @@ function push_cell_atomic!(cell_list::SpatialHashingCellList, cell, point) end end - function deleteat_cell!(cell_list::SpatialHashingCellList, cell, i) deleteat!(cell_list[cell], i) end diff --git a/src/gpu.jl b/src/gpu.jl index cd418c2e..1bcaf8c5 100644 --- a/src/gpu.jl +++ b/src/gpu.jl @@ -8,6 +8,7 @@ # # `Adapt.@adapt_structure` automatically generates the `adapt` function for our custom types. Adapt.@adapt_structure FullGridCellList +Adapt.@adapt_structure SpatialHashingCellList Adapt.@adapt_structure DynamicVectorOfVectors # `adapt(CuArray, ::SVector)::SVector`, but `adapt(Array, ::SVector)::Vector`. From 07420dfa66495670b379873716b0ba2c787d6296 Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Tue, 27 May 2025 14:40:16 +0800 Subject: [PATCH 07/21] Resolve requested changes: - Move functions from cell_lists_util.jl to cell_lists.jl - Change dispatch in `supported_update_strategies()` - Change doc string of SHCL (SpatialHashingCellList) --- src/cell_lists/cell_lists.jl | 35 +++++++++++++++++++++++++++ src/cell_lists/cell_lists_util.jl | 39 ------------------------------- src/cell_lists/full_grid.jl | 2 +- src/cell_lists/spatial_hashing.jl | 24 +++++++++++++++---- 4 files changed, 56 insertions(+), 44 deletions(-) delete mode 100644 src/cell_lists/cell_lists_util.jl diff --git a/src/cell_lists/cell_lists.jl b/src/cell_lists/cell_lists.jl index 7c68760f..773d457f 100644 --- a/src/cell_lists/cell_lists.jl +++ b/src/cell_lists/cell_lists.jl @@ -4,6 +4,41 @@ abstract type AbstractCellList end # able to be used in a threaded loop. @inline each_cell_index_threadable(cell_list::AbstractCellList) = each_cell_index(cell_list) +@inline function check_cell_bounds(cell_list::AbstractCellList, cell::Integer) + (; cells) = cell_list + + checkbounds(cells, cell) +end + +# We need the prod() because FullGridCellList's size is a tuple of cells per dimension whereas +# SpatialHashingCellList's size is an Integer for the number of cells in total. +function construct_backend(::Type{<:AbstractCellList}, ::Type{Vector{Vector{T}}}, + max_outer_length, + max_inner_length) where {T} + return [T[] for _ in 1:max_outer_length] +end + +function construct_backend(::Type{<:AbstractCellList}, ::Type{DynamicVectorOfVectors{T}}, + max_outer_length, + max_inner_length) where {T} + cells = DynamicVectorOfVectors{T}(max_outer_length = max_outer_length, + max_inner_length = max_inner_length) + resize!(cells, max_outer_length) + + return cells +end + +# When `typeof(cell_list.cells)` is passed, we don't pass the type +# `DynamicVectorOfVectors{T}`, but a type `DynamicVectorOfVectors{T1, T2, T3, T4}`. +# While `A{T} <: A{T1, T2}`, this doesn't hold for the types. +# `Type{A{T}} <: Type{A{T1, T2}}` is NOT true. +function construct_backend(cell_list::Type{<:AbstractCellList}, + ::Type{DynamicVectorOfVectors{T1, T2, T3, T4}}, max_outer_length, + max_inner_length) where {T1, T2, T3, T4} + return construct_backend(cell_list, DynamicVectorOfVectors{T1}, max_outer_length, + max_inner_length) +end + include("dictionary.jl") include("full_grid.jl") include("spatial_hashing.jl") diff --git a/src/cell_lists/cell_lists_util.jl b/src/cell_lists/cell_lists_util.jl deleted file mode 100644 index d2aa45fc..00000000 --- a/src/cell_lists/cell_lists_util.jl +++ /dev/null @@ -1,39 +0,0 @@ -@inline function check_cell_bounds(cell_list::AbstractCellList, cell::Integer) - (; cells) = cell_list - - checkbounds(cells, cell) -end - -# This became kinda unnecessary now since we mostly (only) call the function with the hash key (Integer) -# # Move that to spatial_hashing.jl -@inline function check_cell_bounds(cell_list::SpatialHashingCellList, cell::Tuple) - check_cell_bounds(cell_list, spatial_hash(cell, cell_list.list_size)) -end - -# We need the prod() because FullGridCellList's size is a tuple of cells per dimension whereas -# SpatialHashingCellList's size is an Integer for the number of cells in total. -function construct_backend(::Type{<:AbstractCellList}, ::Type{Vector{Vector{T}}}, size, - max_points_per_cell) where {T} - return [T[] for _ in 1:prod(size)] -end - -function construct_backend(::Type{<:AbstractCellList}, ::Type{DynamicVectorOfVectors{T}}, - size, - max_points_per_cell) where {T} - cells = DynamicVectorOfVectors{T}(max_outer_length = prod(size), - max_inner_length = max_points_per_cell) - resize!(cells, prod(size)) - - return cells -end - -# When `typeof(cell_list.cells)` is passed, we don't pass the type -# `DynamicVectorOfVectors{T}`, but a type `DynamicVectorOfVectors{T1, T2, T3, T4}`. -# While `A{T} <: A{T1, T2}`, this doesn't hold for the types. -# `Type{A{T}} <: Type{A{T1, T2}}` is NOT true. -function construct_backend(cell_list::Type{<:AbstractCellList}, - ::Type{DynamicVectorOfVectors{T1, T2, T3, T4}}, size, - max_points_per_cell) where {T1, T2, T3, T4} - return construct_backend(cell_list, DynamicVectorOfVectors{T1}, size, - max_points_per_cell) -end diff --git a/src/cell_lists/full_grid.jl b/src/cell_lists/full_grid.jl index 9620a69a..2f87448f 100644 --- a/src/cell_lists/full_grid.jl +++ b/src/cell_lists/full_grid.jl @@ -65,7 +65,7 @@ function FullGridCellList(; min_corner, max_corner, n_cells_per_dimension = ceil.(Int, (max_corner .- min_corner) ./ search_radius) linear_indices = LinearIndices(Tuple(n_cells_per_dimension)) - cells = construct_backend(FullGridCellList, backend, n_cells_per_dimension, + cells = construct_backend(FullGridCellList, backend, prod(n_cells_per_dimension), max_points_per_cell) end diff --git a/src/cell_lists/spatial_hashing.jl b/src/cell_lists/spatial_hashing.jl index 44f21f7b..a73099c7 100644 --- a/src/cell_lists/spatial_hashing.jl +++ b/src/cell_lists/spatial_hashing.jl @@ -1,5 +1,7 @@ """ - SpatialHashingCellList{NDIMS}(; list_size) + SpatialHashingCellList{NDIMS}(; list_size, + backend = DynamicVectorOfVectors{Int32}, + max_points_per_cell = 100) A basic spatial hashing implementation. Similar to [`DictionaryCellList`](@ref), the domain is discretized into cells, and the particles in each cell are stored in a hash map. The hash is computed using the spatial location of each cell, @@ -9,7 +11,14 @@ to balance memory consumption against the likelihood of hash collisions. # Arguments - `NDIMS::Int`: Number of spatial dimensions (e.g., `2` or `3`). -- `list_size::Int`: Size of the hash map (e.g., `2 * n_points`) . +- `list_size::Int`: Size of the hash map (e.g., `2 * n_points`). +- `backend = DynamicVectorOfVectors{Int32}`: Type of the data structure to store the actual + cell lists. Can be + - `Vector{Vector{Int32}}`: Scattered memory, but very memory-efficient. + - `DynamicVectorOfVectors{Int32}`: Contiguous memory, optimizing cache-hits. +- `max_points_per_cell = 100`: Maximum number of points per cell. This will be used to + allocate the `DynamicVectorOfVectors`. It is not used with + the `Vector{Vector{Int32}}` backend. """ struct SpatialHashingCellList{NDIMS, CL, CI, CF} <: AbstractCellList @@ -31,8 +40,12 @@ end @inline Base.ndims(::SpatialHashingCellList{NDIMS}) where {NDIMS} = NDIMS +function supported_update_strategies(::SpatialHashingCellList{<:DynamicVectorOfVectors}) + return (ParallelUpdate, SerialUpdate) +end + function supported_update_strategies(::SpatialHashingCellList) - return (SerialUpdate, ParallelUpdate) + return (SerialUpdate) end function SpatialHashingCellList{NDIMS}(list_size, @@ -86,7 +99,6 @@ function push_cell_atomic!(cell_list::SpatialHashingCellList, cell, point) NDIMS = ndims(cell_list) hash_key = spatial_hash(cell, list_size) - # Correct to use hash key? @boundscheck check_cell_bounds(cell_list, hash_key) @inbounds pushat_atomic!(cells, hash_key, point) @@ -148,3 +160,7 @@ function spatial_hash(cell::NTuple{3, Real}, list_size) return mod(xor(i * 73856093, j * 19349663, k * 83492791), list_size) + 1 end + +@inline function check_cell_bounds(cell_list::SpatialHashingCellList, cell::Tuple) + check_cell_bounds(cell_list, spatial_hash(cell, cell_list.list_size)) +end From 24fddaf49eede3563383300ed8ef8687ef27abc2 Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Tue, 27 May 2025 17:20:08 +0800 Subject: [PATCH 08/21] Resolve requested changes: - Add `@inbounds` in `push_cell_atomic!` - Improved type dispatch for `supported_update_strategies` - Clarified and cleaned up cell list initialization and emptying, - General code cleanup. --- src/cell_lists/cell_lists.jl | 12 ++++++++++-- src/cell_lists/full_grid.jl | 9 --------- src/cell_lists/spatial_hashing.jl | 29 +++++++++++++++-------------- src/nhs_grid.jl | 4 ++-- test/cell_lists/spatial_hashing.jl | 2 +- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/cell_lists/cell_lists.jl b/src/cell_lists/cell_lists.jl index 773d457f..494adc35 100644 --- a/src/cell_lists/cell_lists.jl +++ b/src/cell_lists/cell_lists.jl @@ -12,7 +12,7 @@ end # We need the prod() because FullGridCellList's size is a tuple of cells per dimension whereas # SpatialHashingCellList's size is an Integer for the number of cells in total. -function construct_backend(::Type{<:AbstractCellList}, ::Type{Vector{Vector{T}}}, +function construct_backend(::Type{Vector{Vector{T}}}, max_outer_length, max_inner_length) where {T} return [T[] for _ in 1:max_outer_length] @@ -39,7 +39,15 @@ function construct_backend(cell_list::Type{<:AbstractCellList}, max_inner_length) end +function max_points_per_cell(cells::DynamicVectorOfVectors) + return size(cells.backend, 1) +end + +# Fallback when backend is a `Vector{Vector{T}}`. Only used for copying the cell list. +function max_points_per_cell(cells) + return 100 +end + include("dictionary.jl") include("full_grid.jl") include("spatial_hashing.jl") -include("cell_lists_util.jl") diff --git a/src/cell_lists/full_grid.jl b/src/cell_lists/full_grid.jl index 2f87448f..8d6c9dc7 100644 --- a/src/cell_lists/full_grid.jl +++ b/src/cell_lists/full_grid.jl @@ -192,15 +192,6 @@ function copy_cell_list(cell_list::FullGridCellList, search_radius, periodic_box max_points_per_cell = max_points_per_cell(cell_list.cells)) end -function max_points_per_cell(cells::DynamicVectorOfVectors) - return size(cells.backend, 1) -end - -# Fallback when backend is a `Vector{Vector{T}}`. Only used for copying the cell list. -function max_points_per_cell(cells) - return 100 -end - @inline function check_cell_bounds(cell_list::FullGridCellList{<:DynamicVectorOfVectors{<:Any, <:Array}}, cell::Tuple) diff --git a/src/cell_lists/spatial_hashing.jl b/src/cell_lists/spatial_hashing.jl index a73099c7..665b41e9 100644 --- a/src/cell_lists/spatial_hashing.jl +++ b/src/cell_lists/spatial_hashing.jl @@ -40,12 +40,15 @@ end @inline Base.ndims(::SpatialHashingCellList{NDIMS}) where {NDIMS} = NDIMS -function supported_update_strategies(::SpatialHashingCellList{<:DynamicVectorOfVectors}) +function supported_update_strategies(::SpatialHashingCellList{NDIMS, CL, CI, CF}) where {NDIMS, + CL <: + DynamicVectorOfVectors, + CI, + CF} return (ParallelUpdate, SerialUpdate) end - function supported_update_strategies(::SpatialHashingCellList) - return (SerialUpdate) + return (SerialUpdate;) end function SpatialHashingCellList{NDIMS}(list_size, @@ -60,15 +63,15 @@ function SpatialHashingCellList{NDIMS}(list_size, end function Base.empty!(cell_list::SpatialHashingCellList) - (; list_size) = cell_list + (; cells) = cell_list NDIMS = ndims(cell_list) # `Base.empty!.(cells)`, but for all backends - for i in eachindex(cell_list.cells) - emptyat!(cell_list.cells, i) + @threaded default_backend(cells) for i in eachindex(cells) + emptyat!(cells, i) end - cell_list.coords .= [ntuple(_ -> typemin(Int), NDIMS) for _ in 1:list_size] + fill!(cell_list.coords, ntuple(_->typemin(Int), NDIMS)) cell_list.collisions .= false return cell_list end @@ -102,15 +105,15 @@ function push_cell_atomic!(cell_list::SpatialHashingCellList, cell, point) @boundscheck check_cell_bounds(cell_list, hash_key) @inbounds pushat_atomic!(cells, hash_key, point) - cell_coord = coords[hash_key] + cell_coord = @inbounds coords[hash_key] if cell_coord == ntuple(_ -> typemin(Int), NDIMS) + # Throws `bitcast: value not a primitive type`-error + # @inbounds Atomix.@atomic coords[hash_key] = cell # If this cell is not used yet, set cell coordinates - # Atomix.@atomic coords[hash_key] = cell - coords[hash_key] = cell + @inbounds coords[hash_key] = cell elseif cell_coord != cell # If it is already used by a different cell, mark as collision - # Atomix.@atomic collisions[hash_key] = true - collisions[hash_key] = true + @inbounds Atomix.@atomic collisions[hash_key] = true end end @@ -125,8 +128,6 @@ function copy_cell_list(cell_list::SpatialHashingCellList, search_radius, (; list_size) = cell_list NDIMS = ndims(cell_list) - # Here I'm using max_points_per_cell which is defined in src/cell_lists/full_grid.jl - # Think about putting it somewhere all cell list can access it or copying it here return SpatialHashingCellList{NDIMS}(list_size, typeof(cell_list.cells), max_points_per_cell(cell_list.cells)) end diff --git a/src/nhs_grid.jl b/src/nhs_grid.jl index b1af37de..f437f95c 100644 --- a/src/nhs_grid.jl +++ b/src/nhs_grid.jl @@ -379,8 +379,8 @@ end # Fully parallel incremental update with atomic push. # TODO `cell_list.cells.lengths` and `cell_list.cells.backend` are hardcoded -# for `FullGridCellList` and `SpatialHashingCellList`, which are currently -# the only implementations supporting this update strategy. +# for `FullGridCellList`, which is currently the only implementation +# supporting this update strategy. function update_grid!(neighborhood_search::GridNeighborhoodSearch{<:Any, ParallelIncrementalUpdate}, y::AbstractMatrix; parallelization_backend = default_backend(y), diff --git a/test/cell_lists/spatial_hashing.jl b/test/cell_lists/spatial_hashing.jl index 2c83f611..a48ac4bd 100644 --- a/test/cell_lists/spatial_hashing.jl +++ b/test/cell_lists/spatial_hashing.jl @@ -52,7 +52,7 @@ points1 = nhs.cell_list[cell1] points2 = nhs.cell_list[cell2] - @test points1 == points2 == [1, 2] + @test sort(points1) == sort(points2) == [1, 2] @test cell1_hash == cell2_hash end From ea1b2614e42123b0da937b49c8a6f1c38e934f38 Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Thu, 29 May 2025 15:52:40 +0800 Subject: [PATCH 09/21] Add struct StaticVectorOfVectors (SVOV) Add basic test for SVOV WIP to adapt FullGridCellList and GridNHS to use SVOV --- .gitignore | 2 + src/cell_lists/cell_lists.jl | 16 +++- src/cell_lists/spatial_hashing.jl | 1 - src/nhs_grid.jl | 34 +++++++ src/vector_of_vectors.jl | 66 ++++++++++++++ test/vector_of_vectors.jl | 144 +++++++++++++++++------------- 6 files changed, 197 insertions(+), 66 deletions(-) diff --git a/.gitignore b/.gitignore index aebf3c39..514298cd 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ out/* .DS_Store LocalPreferences.toml + +benchmarks/plot.jl diff --git a/src/cell_lists/cell_lists.jl b/src/cell_lists/cell_lists.jl index 494adc35..3cd86cab 100644 --- a/src/cell_lists/cell_lists.jl +++ b/src/cell_lists/cell_lists.jl @@ -10,14 +10,24 @@ abstract type AbstractCellList end checkbounds(cells, cell) end -# We need the prod() because FullGridCellList's size is a tuple of cells per dimension whereas -# SpatialHashingCellList's size is an Integer for the number of cells in total. -function construct_backend(::Type{Vector{Vector{T}}}, +function construct_backend(::Type{<:AbstractCellList}, ::Type{Vector{Vector{T}}}, max_outer_length, max_inner_length) where {T} return [T[] for _ in 1:max_outer_length] end +function construct_backend(::Type{<:AbstractCellList}, ::Type{StaticVectorOfVectors{T}}, + max_outer_length, + n_points) where {T} + cells = StaticVectorOfVectors{T}(n_values = n_points, + n_bins = max_outer_length) + # resize!(cells, max_outer_length) + + return cells +end + + + function construct_backend(::Type{<:AbstractCellList}, ::Type{DynamicVectorOfVectors{T}}, max_outer_length, max_inner_length) where {T} diff --git a/src/cell_lists/spatial_hashing.jl b/src/cell_lists/spatial_hashing.jl index 665b41e9..77db815f 100644 --- a/src/cell_lists/spatial_hashing.jl +++ b/src/cell_lists/spatial_hashing.jl @@ -83,7 +83,6 @@ function push_cell!(cell_list::SpatialHashingCellList, cell, point) NDIMS = ndims(cell_list) hash_key = spatial_hash(cell, list_size) - # Correct to use hash key? @boundscheck check_cell_bounds(cell_list, hash_key) @inbounds pushat!(cells, hash_key, point) diff --git a/src/nhs_grid.jl b/src/nhs_grid.jl index f437f95c..8fd916cc 100644 --- a/src/nhs_grid.jl +++ b/src/nhs_grid.jl @@ -227,6 +227,40 @@ function initialize_grid!(neighborhood_search::GridNeighborhoodSearch, y::Abstra return neighborhood_search end +# Initialize for StaticVectorOfVectors, since we don't support push() +function initialize_grid!(neighborhood_search::GridNeighborhoodSearch{NDIMS, US, CL, ELTYPE, + PB, UB}, + y::AbstractMatrix; + parallelization_backend = default_backend(y), + eachindex_y = axes(y, 2)) where {C <: StaticVectorOfVectors, + LI, MINC, MAXC, + CL <: + FullGridCellList{C, LI, MINC, + MAXC}, + NDIMS, US, ELTYPE, PB, UB} + @info "initialize_grid! with StaticVectorOfVectors" + + (; cell_list) = neighborhood_search + (; cells) = cell_list + + if neighborhood_search.search_radius < eps() + # Cannot initialize with zero search radius. + # This is used in TrixiParticles when a neighborhood search is not used. + return neighborhood_search + end + + @boundscheck checkbounds(y, eachindex_y) + points = collect(eachindex_y) + + function point_to_cell(point) + point_coords = @inbounds extract_svector(y, Val(ndims(neighborhood_search)), point) + return cell_coords(point_coords, neighborhood_search) + end + update!(cells, point_to_cell) + + return neighborhood_search +end + function initialize_grid!(neighborhood_search::GridNeighborhoodSearch{<:Any, ParallelUpdate}, y::AbstractMatrix; parallelization_backend = default_backend(y), diff --git a/src/vector_of_vectors.jl b/src/vector_of_vectors.jl index b8fcd09f..488ed176 100644 --- a/src/vector_of_vectors.jl +++ b/src/vector_of_vectors.jl @@ -161,3 +161,69 @@ end return vov end + +# As opposed to `DynamicVectorOfVectors`, this data structure does not support +# modifying operations like `push!`. +# It can be updated by assigning each contained integer value a new bin index. +mutable struct StaticVectorOfVectors{T, V, L, I} + backend :: V # Vector{Int} containing all values sorted by bin + n_bins :: L # Ref{Int32}: Number of bins + first_bin_index :: I # Vector{Int} containing the first index in `values` of each bin + + # This constructor is necessary for Adapt.jl to work with this struct. + # See the comments in gpu.jl for more details. + function StaticVectorOfVectors(backend::V, n_bins::L, first_bin_index::I) where {T, V, L, I} + new{T,V,L,I}(backend, n_bins, first_bin_index) + end +end + +# Outer constructor that only takes n_values and n_bins, leaves backend = nothing +function StaticVectorOfVectors{T}(; n_bins::Int) where {T} + fbi = Vector{Int}(undef, n_bins) + fbi[1] = 1 # required for the update + backend = nothing + n_bins = Ref{Int32}(n_bins) + + return StaticVectorOfVectors{T, typeof(backend), typeof(n_bins), typeof(first_bin_index)}( + backend, # backend + n_bins, # n_bins + fbi # first_bin_index + ) +end + +@inline Base.size(vov::StaticVectorOfVectors) = (vov.n_bins[],) + +@inline function Base.getindex(vov::StaticVectorOfVectors, i) + (; backend, first_bin_index) = vov + + start = first_bin_index[i] + + if i == length(first_bin_index) + return view(backend, start:length(backend)) + end + + stop = first_bin_index[i + 1] - 1 + + return view(backend, start:stop) +end + +# Used to initialize the data +@inline function initialize!(vov::StaticVectorOfVectors, values) + vov.backend = values + return vov +end + +@inline function update!(vov::StaticVectorOfVectors, f) + (; backend, first_bin_index, n_bins) = vov + + # TODO figure out how to do that fast and on the GPU + sort!(backend, by = f) + + # TODO figure out how to do that fast and on the GPU + n_particles_per_cell = [count(x -> f(x) == j, backend) for j in 1:n_bins[]] + # Add 1 since first_bin_index starts at 1 + n_particles_per_cell[1] += 1 + + # TODO avoid allocations + first_bin_index[2:(end - 1)] .= cumsum(n_particles_per_cell)[1:(end - 1)] +end diff --git a/test/vector_of_vectors.jl b/test/vector_of_vectors.jl index f35c9203..9e9054f0 100644 --- a/test/vector_of_vectors.jl +++ b/test/vector_of_vectors.jl @@ -1,85 +1,105 @@ -@testset verbose=true "`DynamicVectorOfVectors`" begin - # Test different types by defining a function to convert to this type - types = [Int32, Float64, i -> (i, i)] +@testset verbose=true "`StaticVectorOfVectors`" begin + n_bins = 3 + values = [2, 3, 5, 1, 4] + vov = PointNeighbors.StaticVectorOfVectors{Int}(n_bins=n_bins) + vov = initialize!(vov, values) + + # Fill values and assign bins + f(x) = x % n_bins + 1 + PointNeighbors.update!(vov, f) + + # Test bin sizes + for i in 1:n_bins + bin = vov[i] + @test all(f(x) == i for x in bin) + end - @testset verbose=true "Eltype $(eltype(type(1)))" for type in types - ELTYPE = typeof(type(1)) - vov_ref = Vector{Vector{ELTYPE}}() - vov = PointNeighbors.DynamicVectorOfVectors{ELTYPE}(max_outer_length = 20, - max_inner_length = 4) + # Test that all values are present + @test sort(vcat([collect(vov[i]) for i in 1:n_bins]...)) == sort(vov.backend) +end - # Test internal size - @test size(vov.backend) == (4, 20) +# @testset verbose=true "`DynamicVectorOfVectors`" begin +# # Test different types by defining a function to convert to this type +# types = [Int32, Float64, i -> (i, i)] - function verify(vov, vov_ref) - @test length(vov) == length(vov_ref) - @test eachindex(vov) == eachindex(vov_ref) - @test axes(vov) == axes(vov_ref) +# @testset verbose=true "Eltype $(eltype(type(1)))" for type in types +# ELTYPE = typeof(type(1)) +# vov_ref = Vector{Vector{ELTYPE}}() +# vov = PointNeighbors.DynamicVectorOfVectors{ELTYPE}(max_outer_length = 20, +# max_inner_length = 4) - @test_throws BoundsError vov[0] - @test_throws BoundsError vov[length(vov) + 1] +# # Test internal size +# @test size(vov.backend) == (4, 20) - for i in eachindex(vov_ref) - @test vov[i] == vov_ref[i] - end - end +# function verify(vov, vov_ref) +# @test length(vov) == length(vov_ref) +# @test eachindex(vov) == eachindex(vov_ref) +# @test axes(vov) == axes(vov_ref) - # Initial check - verify(vov, vov_ref) +# @test_throws BoundsError vov[0] +# @test_throws BoundsError vov[length(vov) + 1] - # First `push!` - push!(vov_ref, type.([1, 2, 3])) - push!(vov, type.([1, 2, 3])) +# for i in eachindex(vov_ref) +# @test vov[i] == vov_ref[i] +# end +# end - verify(vov, vov_ref) +# # Initial check +# verify(vov, vov_ref) - # `push!` multiple items - push!(vov_ref, type.([4]), type.([5, 6, 7, 8])) - push!(vov, type.([4]), type.([5, 6, 7, 8])) +# # First `push!` +# push!(vov_ref, type.([1, 2, 3])) +# push!(vov, type.([1, 2, 3])) - verify(vov, vov_ref) +# verify(vov, vov_ref) - # `push!` to an inner vector - push!(vov_ref[1], type(12)) - PointNeighbors.pushat!(vov, 1, type(12)) +# # `push!` multiple items +# push!(vov_ref, type.([4]), type.([5, 6, 7, 8])) +# push!(vov, type.([4]), type.([5, 6, 7, 8])) - # `push!` overflow - error_ = ErrorException("cell list is full. Use a larger `max_points_per_cell`.") - @test_throws error_ PointNeighbors.pushat!(vov, 1, type(13)) +# verify(vov, vov_ref) - verify(vov, vov_ref) +# # `push!` to an inner vector +# push!(vov_ref[1], type(12)) +# PointNeighbors.pushat!(vov, 1, type(12)) - # Delete entry of inner vector. Note that this changes the order of the elements. - deleteat!(vov_ref[3], 2) - PointNeighbors.deleteatat!(vov, 3, 2) +# # `push!` overflow +# error_ = ErrorException("cell list is full. Use a larger `max_points_per_cell`.") +# @test_throws error_ PointNeighbors.pushat!(vov, 1, type(13)) - @test vov_ref[3] == type.([5, 7, 8]) - @test vov[3] == type.([5, 8, 7]) +# verify(vov, vov_ref) - # Delete second to last entry - deleteat!(vov_ref[3], 2) - PointNeighbors.deleteatat!(vov, 3, 2) +# # Delete entry of inner vector. Note that this changes the order of the elements. +# deleteat!(vov_ref[3], 2) +# PointNeighbors.deleteatat!(vov, 3, 2) - @test vov_ref[3] == type.([5, 8]) - @test vov[3] == type.([5, 7]) +# @test vov_ref[3] == type.([5, 7, 8]) +# @test vov[3] == type.([5, 8, 7]) - # Delete last entry - deleteat!(vov_ref[3], 2) - PointNeighbors.deleteatat!(vov, 3, 2) +# # Delete second to last entry +# deleteat!(vov_ref[3], 2) +# PointNeighbors.deleteatat!(vov, 3, 2) - # Now they are identical again - verify(vov, vov_ref) +# @test vov_ref[3] == type.([5, 8]) +# @test vov[3] == type.([5, 7]) - # Delete the remaining entry of this vector - deleteat!(vov_ref[3], 1) - PointNeighbors.deleteatat!(vov, 3, 1) +# # Delete last entry +# deleteat!(vov_ref[3], 2) +# PointNeighbors.deleteatat!(vov, 3, 2) - verify(vov, vov_ref) +# # Now they are identical again +# verify(vov, vov_ref) - # `empty!` - empty!(vov_ref) - empty!(vov) +# # Delete the remaining entry of this vector +# deleteat!(vov_ref[3], 1) +# PointNeighbors.deleteatat!(vov, 3, 1) - verify(vov, vov_ref) - end -end +# verify(vov, vov_ref) + +# # `empty!` +# empty!(vov_ref) +# empty!(vov) + +# verify(vov, vov_ref) +# end +# end From 2933070b520908ab19d99a027d809b6cc220c475 Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Thu, 29 May 2025 23:46:51 +0800 Subject: [PATCH 10/21] Rename to CompactVectorOfVectors (CVOV) Add basic unit test and CVOV to `test/neighborhood_search.jl` (tests are succeeding) Add update_grid!() and initialize_grid!() --- src/cell_lists/cell_lists.jl | 28 +++++------ src/cell_lists/full_grid.jl | 5 +- src/nhs_grid.jl | 52 +++++++++++--------- src/vector_of_vectors.jl | 59 +++++++++-------------- test/neighborhood_search.jl | 30 +++++++++++- test/vector_of_vectors.jl | 93 ++---------------------------------- 6 files changed, 103 insertions(+), 164 deletions(-) diff --git a/src/cell_lists/cell_lists.jl b/src/cell_lists/cell_lists.jl index 3cd86cab..4a4a3f63 100644 --- a/src/cell_lists/cell_lists.jl +++ b/src/cell_lists/cell_lists.jl @@ -10,25 +10,18 @@ abstract type AbstractCellList end checkbounds(cells, cell) end -function construct_backend(::Type{<:AbstractCellList}, ::Type{Vector{Vector{T}}}, +function construct_backend(_, ::Type{Vector{Vector{T}}}, max_outer_length, max_inner_length) where {T} return [T[] for _ in 1:max_outer_length] end -function construct_backend(::Type{<:AbstractCellList}, ::Type{StaticVectorOfVectors{T}}, - max_outer_length, - n_points) where {T} - cells = StaticVectorOfVectors{T}(n_values = n_points, - n_bins = max_outer_length) - # resize!(cells, max_outer_length) - - return cells +function construct_backend(_, ::Type{CompactVectorOfVectors{T}}, + max_outer_length, _) where {T} + return CompactVectorOfVectors{T}(n_bins = max_outer_length) end - - -function construct_backend(::Type{<:AbstractCellList}, ::Type{DynamicVectorOfVectors{T}}, +function construct_backend(_, ::Type{DynamicVectorOfVectors{T}}, max_outer_length, max_inner_length) where {T} cells = DynamicVectorOfVectors{T}(max_outer_length = max_outer_length, @@ -42,13 +35,20 @@ end # `DynamicVectorOfVectors{T}`, but a type `DynamicVectorOfVectors{T1, T2, T3, T4}`. # While `A{T} <: A{T1, T2}`, this doesn't hold for the types. # `Type{A{T}} <: Type{A{T1, T2}}` is NOT true. -function construct_backend(cell_list::Type{<:AbstractCellList}, - ::Type{DynamicVectorOfVectors{T1, T2, T3, T4}}, max_outer_length, +function construct_backend(cell_list, ::Type{DynamicVectorOfVectors{T1, T2, T3, T4}}, + max_outer_length, max_inner_length) where {T1, T2, T3, T4} return construct_backend(cell_list, DynamicVectorOfVectors{T1}, max_outer_length, max_inner_length) end +function construct_backend(cell_list, ::Type{CompactVectorOfVectors{T1, T2, T3, T4}}, + max_outer_length, + max_inner_length) where {T1, T2, T3, T4} + return construct_backend(cell_list, CompactVectorOfVectors{T1}, max_outer_length, + max_inner_length) +end + function max_points_per_cell(cells::DynamicVectorOfVectors) return size(cells.backend, 1) end diff --git a/src/cell_lists/full_grid.jl b/src/cell_lists/full_grid.jl index 8d6c9dc7..bb16166e 100644 --- a/src/cell_lists/full_grid.jl +++ b/src/cell_lists/full_grid.jl @@ -41,6 +41,10 @@ function supported_update_strategies(::FullGridCellList{<:DynamicVectorOfVectors SerialIncrementalUpdate, SerialUpdate) end +function supported_update_strategies(::FullGridCellList{<:CompactVectorOfVectors}) + return (ParallelUpdate, SerialUpdate) +end + function supported_update_strategies(::FullGridCellList) return (SemiParallelUpdate, SerialIncrementalUpdate, SerialUpdate) end @@ -186,7 +190,6 @@ end function copy_cell_list(cell_list::FullGridCellList, search_radius, periodic_box) (; min_corner, max_corner) = cell_list - return FullGridCellList(; min_corner, max_corner, search_radius, backend = typeof(cell_list.cells), max_points_per_cell = max_points_per_cell(cell_list.cells)) diff --git a/src/nhs_grid.jl b/src/nhs_grid.jl index 8fd916cc..8c0e7ad0 100644 --- a/src/nhs_grid.jl +++ b/src/nhs_grid.jl @@ -227,21 +227,18 @@ function initialize_grid!(neighborhood_search::GridNeighborhoodSearch, y::Abstra return neighborhood_search end -# Initialize for StaticVectorOfVectors, since we don't support push() -function initialize_grid!(neighborhood_search::GridNeighborhoodSearch{NDIMS, US, CL, ELTYPE, - PB, UB}, +# CompactVectorOfVectors +function initialize_grid!(neighborhood_search::GridNeighborhoodSearch{<:Any, ParallelUpdate, + <:FullGridCellList{<:CompactVectorOfVectors}}, y::AbstractMatrix; parallelization_backend = default_backend(y), - eachindex_y = axes(y, 2)) where {C <: StaticVectorOfVectors, - LI, MINC, MAXC, - CL <: - FullGridCellList{C, LI, MINC, - MAXC}, - NDIMS, US, ELTYPE, PB, UB} - @info "initialize_grid! with StaticVectorOfVectors" - + eachindex_y = axes(y, 2)) (; cell_list) = neighborhood_search - (; cells) = cell_list + + if eachindex_y != axes(y, 2) + # Incremental update doesn't support inactive points + error("this neighborhood search/update strategy does not support inactive points") + end if neighborhood_search.search_radius < eps() # Cannot initialize with zero search radius. @@ -249,15 +246,8 @@ function initialize_grid!(neighborhood_search::GridNeighborhoodSearch{NDIMS, US, return neighborhood_search end - @boundscheck checkbounds(y, eachindex_y) - points = collect(eachindex_y) - - function point_to_cell(point) - point_coords = @inbounds extract_svector(y, Val(ndims(neighborhood_search)), point) - return cell_coords(point_coords, neighborhood_search) - end - update!(cells, point_to_cell) - + resize!(cell_list.cells.values, size(y, 2)) + cell_list.cells.values .= eachindex_y return neighborhood_search end @@ -486,6 +476,25 @@ function update_grid!(neighborhood_search::Union{GridNeighborhoodSearch{<:Any, initialize_grid!(neighborhood_search, y; parallelization_backend, eachindex_y) end +# CompactVectorOfVectors +function update_grid!(neighborhood_search::GridNeighborhoodSearch{<:Any, ParallelUpdate, + <:FullGridCellList{<:CompactVectorOfVectors}}, + y::AbstractMatrix; parallelization_backend = default_backend(y), + eachindex_y = axes(y, 2)) + if eachindex_y != axes(y, 2) + # Incremental update doesn't support inactive points + error("this neighborhood search/update strategy does not support inactive points") + end + + @inline function point_to_cell(point) + point_coords = @inbounds extract_svector(y, Val(ndims(neighborhood_search)), point) + cell = cell_coords(point_coords, neighborhood_search) + return PointNeighbors.cell_index(neighborhood_search.cell_list, cell) + end + + update!(neighborhood_search.cell_list.cells, point_to_cell) +end + function check_collision(neighbor_cell_, neighbor_coords, cell_list, nhs) # This is only relevant for the `SpatialHashingCellList` return false @@ -540,7 +549,6 @@ end for neighbor_ in eachindex(neighbors) neighbor = @inbounds neighbors[neighbor_] - # Making the following `@inbounds` yields a ~2% speedup on an NVIDIA H100. # But we don't know if `neighbor` (extracted from the cell list) is in bounds. neighbor_coords = extract_svector(neighbor_system_coords, diff --git a/src/vector_of_vectors.jl b/src/vector_of_vectors.jl index 488ed176..062dd4b4 100644 --- a/src/vector_of_vectors.jl +++ b/src/vector_of_vectors.jl @@ -165,65 +165,52 @@ end # As opposed to `DynamicVectorOfVectors`, this data structure does not support # modifying operations like `push!`. # It can be updated by assigning each contained integer value a new bin index. -mutable struct StaticVectorOfVectors{T, V, L, I} - backend :: V # Vector{Int} containing all values sorted by bin +struct CompactVectorOfVectors{T, V, L, I} + values :: V # Vector{Int} containing all values sorted by bin n_bins :: L # Ref{Int32}: Number of bins first_bin_index :: I # Vector{Int} containing the first index in `values` of each bin # This constructor is necessary for Adapt.jl to work with this struct. # See the comments in gpu.jl for more details. - function StaticVectorOfVectors(backend::V, n_bins::L, first_bin_index::I) where {T, V, L, I} - new{T,V,L,I}(backend, n_bins, first_bin_index) + function CompactVectorOfVectors{T}(values, n_bins, first_bin_index) where {T} + new{T, typeof(values), typeof(n_bins), typeof(first_bin_index)}(values, n_bins, + first_bin_index) end end -# Outer constructor that only takes n_values and n_bins, leaves backend = nothing -function StaticVectorOfVectors{T}(; n_bins::Int) where {T} - fbi = Vector{Int}(undef, n_bins) - fbi[1] = 1 # required for the update - backend = nothing +function CompactVectorOfVectors{T}(; n_bins::Int) where {T} + first_bin_index = Vector{Int}(undef, n_bins+1) + first_bin_index[1] = 1 # required for the update + values = Vector{T}(undef, 0) n_bins = Ref{Int32}(n_bins) - return StaticVectorOfVectors{T, typeof(backend), typeof(n_bins), typeof(first_bin_index)}( - backend, # backend - n_bins, # n_bins - fbi # first_bin_index - ) + return CompactVectorOfVectors{T}(values, # backend Array{T}(...) + n_bins, # n_bins + first_bin_index) end -@inline Base.size(vov::StaticVectorOfVectors) = (vov.n_bins[],) +# Mhh should this may be changed to length(vov.values)? +@inline Base.size(vov::CompactVectorOfVectors) = (vov.n_bins[],) -@inline function Base.getindex(vov::StaticVectorOfVectors, i) - (; backend, first_bin_index) = vov - - start = first_bin_index[i] - - if i == length(first_bin_index) - return view(backend, start:length(backend)) - end +@inline function Base.getindex(vov::CompactVectorOfVectors, i) + (; values, first_bin_index) = vov + start = first_bin_index[i] stop = first_bin_index[i + 1] - 1 - - return view(backend, start:stop) -end - -# Used to initialize the data -@inline function initialize!(vov::StaticVectorOfVectors, values) - vov.backend = values - return vov + return view(values, start:stop) end -@inline function update!(vov::StaticVectorOfVectors, f) - (; backend, first_bin_index, n_bins) = vov +@inline function update!(vov::CompactVectorOfVectors, f) + (; values, first_bin_index, n_bins) = vov # TODO figure out how to do that fast and on the GPU - sort!(backend, by = f) + sort!(values, by = f) # TODO figure out how to do that fast and on the GPU - n_particles_per_cell = [count(x -> f(x) == j, backend) for j in 1:n_bins[]] + n_particles_per_cell = [count(x -> f(x) == j, values) for j in 1:n_bins[]] # Add 1 since first_bin_index starts at 1 n_particles_per_cell[1] += 1 # TODO avoid allocations - first_bin_index[2:(end - 1)] .= cumsum(n_particles_per_cell)[1:(end - 1)] + first_bin_index[2:end] .= cumsum(n_particles_per_cell) end diff --git a/test/neighborhood_search.jl b/test/neighborhood_search.jl index a1ef9b3b..762db47f 100644 --- a/test/neighborhood_search.jl +++ b/test/neighborhood_search.jl @@ -57,6 +57,12 @@ max_corner, search_radius, backend = Vector{Vector{Int32}})), + GridNeighborhoodSearch{NDIMS}(; search_radius, n_points, + periodic_box = periodic_boxes[i], + cell_list = FullGridCellList(; min_corner, + max_corner, + search_radius, + backend = PointNeighbors.CompactVectorOfVectors{Int32})), PrecomputedNeighborhoodSearch{NDIMS}(; search_radius, n_points, periodic_box = periodic_boxes[i]), GridNeighborhoodSearch{NDIMS}(; search_radius, n_points, @@ -70,6 +76,7 @@ "`GridNeighborhoodSearch`", "`GridNeighborhoodSearch` with `FullGridCellList` with `DynamicVectorOfVectors`", "`GridNeighborhoodSearch` with `FullGridCellList` with `Vector{Vector}`", + "`GridNeighborhoodSearch` with `FullGridCellList` with `CompactVectorOfVectors`", "`PrecomputedNeighborhoodSearch`", "`GridNeighborhoodSearch` with `SpatialHashingCellList`" ] @@ -85,6 +92,10 @@ cell_list = FullGridCellList(min_corner = periodic_boxes[i].min_corner, max_corner = periodic_boxes[i].max_corner, backend = Vector{Vector{Int32}})), + GridNeighborhoodSearch{NDIMS}(periodic_box = periodic_boxes[i], + cell_list = FullGridCellList(min_corner = periodic_boxes[i].min_corner, + max_corner = periodic_boxes[i].max_corner, + backend = PointNeighbors.CompactVectorOfVectors{Int32})), PrecomputedNeighborhoodSearch{NDIMS}(periodic_box = periodic_boxes[i]), GridNeighborhoodSearch{NDIMS}(periodic_box = periodic_boxes[i], cell_list = SpatialHashingCellList{NDIMS}(2 * @@ -102,6 +113,11 @@ initialize!(nhs, coords, coords) + if nhs isa GridNeighborhoodSearch && nhs.cell_list isa FullGridCellList && + nhs.cell_list.cells isa PointNeighbors.CompactVectorOfVectors + update!(nhs, coords, coords) + end + neighbors = [Int[] for _ in axes(coords, 2)] foreach_point_neighbor(coords, coords, nhs, @@ -121,7 +137,6 @@ end end end - @testset verbose=true "Compare Against `TrivialNeighborhoodSearch`" begin cloud_sizes = [ (10, 11), @@ -192,6 +207,11 @@ max_corner, search_radius, backend = Vector{Vector{Int}})), + GridNeighborhoodSearch{NDIMS}(; search_radius, n_points, + cell_list = FullGridCellList(; min_corner, + max_corner, + search_radius, + backend = PointNeighbors.CompactVectorOfVectors{Int32})), PrecomputedNeighborhoodSearch{NDIMS}(; search_radius, n_points), GridNeighborhoodSearch{NDIMS}(; search_radius, n_points, cell_list = SpatialHashingCellList{NDIMS}(2 * @@ -206,6 +226,7 @@ "`GridNeighborhoodSearch` with `FullGridCellList` with `DynamicVectorOfVectors` and `ParallelIncrementalUpdate`", "`GridNeighborhoodSearch` with `FullGridCellList` with `DynamicVectorOfVectors` and `SemiParallelUpdate`", "`GridNeighborhoodSearch` with `FullGridCellList` with `Vector{Vector}`", + "`GridNeighborhoodSearch` with `FullGridCellList` with `CompactVectorOfVectors`", "`PrecomputedNeighborhoodSearch`", "`GridNeighborhoodSearch` with `SpatialHashingCellList`" ] @@ -226,6 +247,9 @@ GridNeighborhoodSearch{NDIMS}(cell_list = FullGridCellList(; min_corner, max_corner, backend = Vector{Vector{Int32}})), + GridNeighborhoodSearch{NDIMS}(cell_list = FullGridCellList(; min_corner, + max_corner, + backend = PointNeighbors.CompactVectorOfVectors{Int32})), PrecomputedNeighborhoodSearch{NDIMS}(), GridNeighborhoodSearch{NDIMS}(cell_list = SpatialHashingCellList{NDIMS}(2 * n_points)) @@ -245,7 +269,9 @@ # For other seeds, update with the correct coordinates. # This way, we test only `initialize!` when `seed == 1`, # and `initialize!` plus `update!` else. - if seed != 1 + if seed != 1 || + (nhs isa GridNeighborhoodSearch && nhs.cell_list isa FullGridCellList && + nhs.cell_list.cells isa PointNeighbors.CompactVectorOfVectors) update!(nhs, coords, coords) end diff --git a/test/vector_of_vectors.jl b/test/vector_of_vectors.jl index 9e9054f0..5252c5e4 100644 --- a/test/vector_of_vectors.jl +++ b/test/vector_of_vectors.jl @@ -1,8 +1,9 @@ @testset verbose=true "`StaticVectorOfVectors`" begin n_bins = 3 values = [2, 3, 5, 1, 4] - vov = PointNeighbors.StaticVectorOfVectors{Int}(n_bins=n_bins) - vov = initialize!(vov, values) + vov = PointNeighbors.CompactVectorOfVectors{Int}(n_bins = n_bins) + resize!(vov.values, length(values)) + vov.values .= eachindex(values) # Fill values and assign bins f(x) = x % n_bins + 1 @@ -15,91 +16,5 @@ end # Test that all values are present - @test sort(vcat([collect(vov[i]) for i in 1:n_bins]...)) == sort(vov.backend) + @test sort(vcat([collect(vov[i]) for i in 1:n_bins]...)) == sort(vov.values) end - -# @testset verbose=true "`DynamicVectorOfVectors`" begin -# # Test different types by defining a function to convert to this type -# types = [Int32, Float64, i -> (i, i)] - -# @testset verbose=true "Eltype $(eltype(type(1)))" for type in types -# ELTYPE = typeof(type(1)) -# vov_ref = Vector{Vector{ELTYPE}}() -# vov = PointNeighbors.DynamicVectorOfVectors{ELTYPE}(max_outer_length = 20, -# max_inner_length = 4) - -# # Test internal size -# @test size(vov.backend) == (4, 20) - -# function verify(vov, vov_ref) -# @test length(vov) == length(vov_ref) -# @test eachindex(vov) == eachindex(vov_ref) -# @test axes(vov) == axes(vov_ref) - -# @test_throws BoundsError vov[0] -# @test_throws BoundsError vov[length(vov) + 1] - -# for i in eachindex(vov_ref) -# @test vov[i] == vov_ref[i] -# end -# end - -# # Initial check -# verify(vov, vov_ref) - -# # First `push!` -# push!(vov_ref, type.([1, 2, 3])) -# push!(vov, type.([1, 2, 3])) - -# verify(vov, vov_ref) - -# # `push!` multiple items -# push!(vov_ref, type.([4]), type.([5, 6, 7, 8])) -# push!(vov, type.([4]), type.([5, 6, 7, 8])) - -# verify(vov, vov_ref) - -# # `push!` to an inner vector -# push!(vov_ref[1], type(12)) -# PointNeighbors.pushat!(vov, 1, type(12)) - -# # `push!` overflow -# error_ = ErrorException("cell list is full. Use a larger `max_points_per_cell`.") -# @test_throws error_ PointNeighbors.pushat!(vov, 1, type(13)) - -# verify(vov, vov_ref) - -# # Delete entry of inner vector. Note that this changes the order of the elements. -# deleteat!(vov_ref[3], 2) -# PointNeighbors.deleteatat!(vov, 3, 2) - -# @test vov_ref[3] == type.([5, 7, 8]) -# @test vov[3] == type.([5, 8, 7]) - -# # Delete second to last entry -# deleteat!(vov_ref[3], 2) -# PointNeighbors.deleteatat!(vov, 3, 2) - -# @test vov_ref[3] == type.([5, 8]) -# @test vov[3] == type.([5, 7]) - -# # Delete last entry -# deleteat!(vov_ref[3], 2) -# PointNeighbors.deleteatat!(vov, 3, 2) - -# # Now they are identical again -# verify(vov, vov_ref) - -# # Delete the remaining entry of this vector -# deleteat!(vov_ref[3], 1) -# PointNeighbors.deleteatat!(vov, 3, 1) - -# verify(vov, vov_ref) - -# # `empty!` -# empty!(vov_ref) -# empty!(vov) - -# verify(vov, vov_ref) -# end -# end From a9684e9b6a31bfeffa1e0bbf37aaa7f05bf8e0fa Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Wed, 4 Jun 2025 11:36:42 +0200 Subject: [PATCH 11/21] Update`initialize_grid!()` for CVOV --- src/nhs_grid.jl | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/nhs_grid.jl b/src/nhs_grid.jl index 8c0e7ad0..3da61978 100644 --- a/src/nhs_grid.jl +++ b/src/nhs_grid.jl @@ -227,7 +227,6 @@ function initialize_grid!(neighborhood_search::GridNeighborhoodSearch, y::Abstra return neighborhood_search end -# CompactVectorOfVectors function initialize_grid!(neighborhood_search::GridNeighborhoodSearch{<:Any, ParallelUpdate, <:FullGridCellList{<:CompactVectorOfVectors}}, y::AbstractMatrix; @@ -248,6 +247,10 @@ function initialize_grid!(neighborhood_search::GridNeighborhoodSearch{<:Any, Par resize!(cell_list.cells.values, size(y, 2)) cell_list.cells.values .= eachindex_y + + point_to_cell = point_to_cell_wrapper(neighborhood_search, y) + update!(cell_list.cells, point_to_cell) + return neighborhood_search end @@ -476,7 +479,6 @@ function update_grid!(neighborhood_search::Union{GridNeighborhoodSearch{<:Any, initialize_grid!(neighborhood_search, y; parallelization_backend, eachindex_y) end -# CompactVectorOfVectors function update_grid!(neighborhood_search::GridNeighborhoodSearch{<:Any, ParallelUpdate, <:FullGridCellList{<:CompactVectorOfVectors}}, y::AbstractMatrix; parallelization_backend = default_backend(y), @@ -486,12 +488,7 @@ function update_grid!(neighborhood_search::GridNeighborhoodSearch{<:Any, Paralle error("this neighborhood search/update strategy does not support inactive points") end - @inline function point_to_cell(point) - point_coords = @inbounds extract_svector(y, Val(ndims(neighborhood_search)), point) - cell = cell_coords(point_coords, neighborhood_search) - return PointNeighbors.cell_index(neighborhood_search.cell_list, cell) - end - + point_to_cell = point_to_cell_wrapper(neighborhood_search, y) update!(neighborhood_search.cell_list.cells, point_to_cell) end @@ -646,3 +643,12 @@ function copy_neighborhood_search(nhs::GridNeighborhoodSearch, search_radius, n_ cell_list, update_strategy = nhs.update_strategy) end + +@inline function point_to_cell_wrapper(neighborhood_search, y) + @inline function point_to_cell(point) + point_coords = @inbounds extract_svector(y, Val(ndims(neighborhood_search)), point) + cell = cell_coords(point_coords, neighborhood_search) + return PointNeighbors.cell_index(neighborhood_search.cell_list, cell) + end + return point_to_cell +end From 0acdb4d724f9c94d63ae018e20ac0fa447fc9502 Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Thu, 5 Jun 2025 08:30:07 +0200 Subject: [PATCH 12/21] Remove if-conditions from test for CVOV. --- test/neighborhood_search.jl | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/test/neighborhood_search.jl b/test/neighborhood_search.jl index 762db47f..eb186a18 100644 --- a/test/neighborhood_search.jl +++ b/test/neighborhood_search.jl @@ -113,11 +113,6 @@ initialize!(nhs, coords, coords) - if nhs isa GridNeighborhoodSearch && nhs.cell_list isa FullGridCellList && - nhs.cell_list.cells isa PointNeighbors.CompactVectorOfVectors - update!(nhs, coords, coords) - end - neighbors = [Int[] for _ in axes(coords, 2)] foreach_point_neighbor(coords, coords, nhs, @@ -269,9 +264,7 @@ # For other seeds, update with the correct coordinates. # This way, we test only `initialize!` when `seed == 1`, # and `initialize!` plus `update!` else. - if seed != 1 || - (nhs isa GridNeighborhoodSearch && nhs.cell_list isa FullGridCellList && - nhs.cell_list.cells isa PointNeighbors.CompactVectorOfVectors) + if seed != 1 update!(nhs, coords, coords) end From a004cc599780da00e57cca6b709ee0e7a77777bc Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Thu, 5 Jun 2025 13:50:53 +0200 Subject: [PATCH 13/21] Fix inner constructor of CVOV to work with adapt_structure() --- benchmarks/plot.jl | 86 ---------------------------------------- src/gpu.jl | 1 + src/vector_of_vectors.jl | 11 +++-- 3 files changed, 8 insertions(+), 90 deletions(-) delete mode 100644 benchmarks/plot.jl diff --git a/benchmarks/plot.jl b/benchmarks/plot.jl deleted file mode 100644 index bd7588c3..00000000 --- a/benchmarks/plot.jl +++ /dev/null @@ -1,86 +0,0 @@ -using Plots -using BenchmarkTools - -# Generate a rectangular point cloud -include("../test/point_cloud.jl") - -""" - plot_benchmarks(benchmark, n_points_per_dimension, iterations; - seed = 1, perturbation_factor_position = 1.0, - parallel = true, title = "") - -Run a benchmark with several neighborhood searches multiple times for increasing numbers -of points and plot the results. - -# Arguments -- `benchmark`: The benchmark function. See [`benchmark_count_neighbors`](@ref) - and [`benchmark_n_body`](@ref). -- `n_points_per_dimension`: Initial resolution as tuple. The product is the initial number - of points. For example, use `(100, 100)` for a 2D benchmark or - `(10, 10, 10)` for a 3D benchmark. -- `iterations`: Number of refinement iterations - -# Keywords -- `parallel = true`: Loop over all points in parallel -- `title = ""`: Title of the plot -- `seed = 1`: Seed to perturb the point positions. Different seeds yield - slightly different point positions. -- `perturbation_factor_position = 1.0`: Perturb point positions by this factor. A factor of - `1.0` corresponds to points being moved by - a maximum distance of `0.5` along each axis. - -# Examples -```julia -include("benchmarks/benchmarks.jl") - -plot_benchmarks(benchmark_count_neighbors, (10, 10), 3) -""" -function plot_benchmarks(benchmark, n_points_per_dimension, iterations; - parallelization_backend = PolyesterBackend(), title = "", - seed = 1, perturbation_factor_position = 1.0) - neighborhood_searches_names = ["TrivialNeighborhoodSearch";; - "GridNeighborhoodSearch";; - "PrecomputedNeighborhoodSearch"] - - # Multiply number of points in each iteration (roughly) by this factor - scaling_factor = 4 - per_dimension_factor = scaling_factor^(1 / length(n_points_per_dimension)) - sizes = [round.(Int, n_points_per_dimension .* per_dimension_factor^(iter - 1)) - for iter in 1:iterations] - - n_particles_vec = prod.(sizes) - times = zeros(iterations, length(neighborhood_searches_names)) - - for iter in 1:iterations - coordinates = point_cloud(sizes[iter], seed = seed, - perturbation_factor_position = perturbation_factor_position) - - search_radius = 3.0 - NDIMS = size(coordinates, 1) - n_particles = size(coordinates, 2) - - neighborhood_searches = [ - TrivialNeighborhoodSearch{NDIMS}(; search_radius, eachpoint = 1:n_particles), - GridNeighborhoodSearch{NDIMS}(; search_radius, n_points = n_particles), - PrecomputedNeighborhoodSearch{NDIMS}(; search_radius, n_points = n_particles) - ] - - for i in eachindex(neighborhood_searches) - neighborhood_search = neighborhood_searches[i] - initialize!(neighborhood_search, coordinates, coordinates) - - time = benchmark(neighborhood_search, coordinates; parallelization_backend) - times[iter, i] = time - time_string = BenchmarkTools.prettytime(time * 1e9) - println("$(neighborhood_searches_names[i])") - println("with $(join(sizes[iter], "x")) = $(prod(sizes[iter])) particles finished in $time_string\n") - end - end - - plot(n_particles_vec, times, - xaxis = :log, yaxis = :log, - xticks = (n_particles_vec, n_particles_vec), - xlabel = "#particles", ylabel = "Runtime [s]", - legend = :outerright, size = (750, 400), dpi = 200, - label = neighborhood_searches_names, title = title) -end diff --git a/src/gpu.jl b/src/gpu.jl index 1bcaf8c5..b6e5c5dd 100644 --- a/src/gpu.jl +++ b/src/gpu.jl @@ -10,6 +10,7 @@ Adapt.@adapt_structure FullGridCellList Adapt.@adapt_structure SpatialHashingCellList Adapt.@adapt_structure DynamicVectorOfVectors +Adapt.@adapt_structure CompactVectorOfVectors # `adapt(CuArray, ::SVector)::SVector`, but `adapt(Array, ::SVector)::Vector`. # We don't want to change the type of the `SVector` here. diff --git a/src/vector_of_vectors.jl b/src/vector_of_vectors.jl index 062dd4b4..8f5dbbb2 100644 --- a/src/vector_of_vectors.jl +++ b/src/vector_of_vectors.jl @@ -172,8 +172,8 @@ struct CompactVectorOfVectors{T, V, L, I} # This constructor is necessary for Adapt.jl to work with this struct. # See the comments in gpu.jl for more details. - function CompactVectorOfVectors{T}(values, n_bins, first_bin_index) where {T} - new{T, typeof(values), typeof(n_bins), typeof(first_bin_index)}(values, n_bins, + function CompactVectorOfVectors(values, n_bins, first_bin_index) + new{eltype(values), typeof(values), typeof(n_bins), typeof(first_bin_index)}(values, n_bins, first_bin_index) end end @@ -184,8 +184,8 @@ function CompactVectorOfVectors{T}(; n_bins::Int) where {T} values = Vector{T}(undef, 0) n_bins = Ref{Int32}(n_bins) - return CompactVectorOfVectors{T}(values, # backend Array{T}(...) - n_bins, # n_bins + return CompactVectorOfVectors(values, + n_bins, first_bin_index) end @@ -203,12 +203,15 @@ end @inline function update!(vov::CompactVectorOfVectors, f) (; values, first_bin_index, n_bins) = vov + # Main.@infiltrate + # TODO figure out how to do that fast and on the GPU sort!(values, by = f) # TODO figure out how to do that fast and on the GPU n_particles_per_cell = [count(x -> f(x) == j, values) for j in 1:n_bins[]] # Add 1 since first_bin_index starts at 1 + # Add 1 since first_bin_index starts at 1 n_particles_per_cell[1] += 1 # TODO avoid allocations From e5f3c3be3cf8caacf0c82bd1e9a60e2bb902e4a1 Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Thu, 5 Jun 2025 14:41:35 +0200 Subject: [PATCH 14/21] Benchmark 2 versions of how to compute the number of particles per bin in n_particles_per_cell. --- src/nhs_grid.jl | 2 +- src/vector_of_vectors.jl | 24 +++++++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/nhs_grid.jl b/src/nhs_grid.jl index 3da61978..7645055b 100644 --- a/src/nhs_grid.jl +++ b/src/nhs_grid.jl @@ -650,5 +650,5 @@ end cell = cell_coords(point_coords, neighborhood_search) return PointNeighbors.cell_index(neighborhood_search.cell_list, cell) end - return point_to_cell + return point_to_cell end diff --git a/src/vector_of_vectors.jl b/src/vector_of_vectors.jl index 8f5dbbb2..1f5267b5 100644 --- a/src/vector_of_vectors.jl +++ b/src/vector_of_vectors.jl @@ -173,8 +173,9 @@ struct CompactVectorOfVectors{T, V, L, I} # This constructor is necessary for Adapt.jl to work with this struct. # See the comments in gpu.jl for more details. function CompactVectorOfVectors(values, n_bins, first_bin_index) - new{eltype(values), typeof(values), typeof(n_bins), typeof(first_bin_index)}(values, n_bins, - first_bin_index) + new{eltype(values), typeof(values), typeof(n_bins), typeof(first_bin_index)}(values, + n_bins, + first_bin_index) end end @@ -185,8 +186,8 @@ function CompactVectorOfVectors{T}(; n_bins::Int) where {T} n_bins = Ref{Int32}(n_bins) return CompactVectorOfVectors(values, - n_bins, - first_bin_index) + n_bins, + first_bin_index) end # Mhh should this may be changed to length(vov.values)? @@ -202,15 +203,20 @@ end @inline function update!(vov::CompactVectorOfVectors, f) (; values, first_bin_index, n_bins) = vov - - # Main.@infiltrate - # TODO figure out how to do that fast and on the GPU sort!(values, by = f) # TODO figure out how to do that fast and on the GPU - n_particles_per_cell = [count(x -> f(x) == j, values) for j in 1:n_bins[]] - # Add 1 since first_bin_index starts at 1 + + # @info "[count(x ...)]" + # n_particles_per_cell = [count(x -> f(x) == j, values) for j in 1:n_bins[]] + + @info "for val in values" + n_particles_per_cell = zeros(n_bins[]) + for val in values + n_particles_per_cell[f(val)] += 1 + end + # Add 1 since first_bin_index starts at 1 n_particles_per_cell[1] += 1 From 2211dfe0e2678b35c37afdc13bcd80a1c25cc57b Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Thu, 12 Jun 2025 15:08:14 +0200 Subject: [PATCH 15/21] Minor changes. --- src/vector_of_vectors.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/vector_of_vectors.jl b/src/vector_of_vectors.jl index 1f5267b5..d727ba05 100644 --- a/src/vector_of_vectors.jl +++ b/src/vector_of_vectors.jl @@ -208,10 +208,8 @@ end # TODO figure out how to do that fast and on the GPU - # @info "[count(x ...)]" # n_particles_per_cell = [count(x -> f(x) == j, values) for j in 1:n_bins[]] - @info "for val in values" n_particles_per_cell = zeros(n_bins[]) for val in values n_particles_per_cell[f(val)] += 1 From 62a464af3bd7b1b7dfcd6566a2bccd65e6967ebe Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Thu, 12 Jun 2025 15:13:24 +0200 Subject: [PATCH 16/21] Minor changes. --- .gitignore | 3 +++ Project.toml | 25 ------------------------- 2 files changed, 3 insertions(+), 25 deletions(-) delete mode 100644 Project.toml diff --git a/.gitignore b/.gitignore index 514298cd..3abc1ce9 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ out/* LocalPreferences.toml benchmarks/plot.jl +benchmarks/benchmark_initialize.jl +gpu_env +Project.toml diff --git a/Project.toml b/Project.toml deleted file mode 100644 index ca802606..00000000 --- a/Project.toml +++ /dev/null @@ -1,25 +0,0 @@ -name = "PointNeighbors" -uuid = "1c4d5385-0a27-49de-8e2c-43b175c8985c" -authors = ["Erik Faulhaber "] -version = "0.6.2" - -[deps] -Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" -Atomix = "a9b6321e-bd34-4604-b9c9-b65b8de01458" -GPUArraysCore = "46192b85-c4d5-4398-a991-12ede77f4527" -KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -Polyester = "f517fe37-dbe3-4b94-8317-1923a5111588" -Reexport = "189a3867-3050-52da-a836-e630ba90ab69" -StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" - -[compat] -Adapt = "4" -Atomix = "1" -GPUArraysCore = "0.2" -KernelAbstractions = "0.9" -LinearAlgebra = "1" -Polyester = "0.7.5" -Reexport = "1" -StaticArrays = "1" -julia = "1.10" \ No newline at end of file From 78dfb3c0a814333537f424b8b6361af9a67bd237 Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Thu, 12 Jun 2025 21:16:12 +0800 Subject: [PATCH 17/21] Simplify `construct_backend()`. --- src/cell_lists/cell_lists.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cell_lists/cell_lists.jl b/src/cell_lists/cell_lists.jl index 4a4a3f63..da258a72 100644 --- a/src/cell_lists/cell_lists.jl +++ b/src/cell_lists/cell_lists.jl @@ -10,18 +10,18 @@ abstract type AbstractCellList end checkbounds(cells, cell) end -function construct_backend(_, ::Type{Vector{Vector{T}}}, +function construct_backend(::Type{Vector{Vector{T}}}, max_outer_length, max_inner_length) where {T} return [T[] for _ in 1:max_outer_length] end -function construct_backend(_, ::Type{CompactVectorOfVectors{T}}, +function construct_backend(::Type{CompactVectorOfVectors{T}}, max_outer_length, _) where {T} return CompactVectorOfVectors{T}(n_bins = max_outer_length) end -function construct_backend(_, ::Type{DynamicVectorOfVectors{T}}, +function construct_backend(::Type{DynamicVectorOfVectors{T}}, max_outer_length, max_inner_length) where {T} cells = DynamicVectorOfVectors{T}(max_outer_length = max_outer_length, @@ -35,17 +35,17 @@ end # `DynamicVectorOfVectors{T}`, but a type `DynamicVectorOfVectors{T1, T2, T3, T4}`. # While `A{T} <: A{T1, T2}`, this doesn't hold for the types. # `Type{A{T}} <: Type{A{T1, T2}}` is NOT true. -function construct_backend(cell_list, ::Type{DynamicVectorOfVectors{T1, T2, T3, T4}}, +function construct_backend(::Type{DynamicVectorOfVectors{T1, T2, T3, T4}}, max_outer_length, max_inner_length) where {T1, T2, T3, T4} - return construct_backend(cell_list, DynamicVectorOfVectors{T1}, max_outer_length, + return construct_backend(DynamicVectorOfVectors{T1}, max_outer_length, max_inner_length) end -function construct_backend(cell_list, ::Type{CompactVectorOfVectors{T1, T2, T3, T4}}, +function construct_backend(::Type{CompactVectorOfVectors{T1, T2, T3, T4}}, max_outer_length, max_inner_length) where {T1, T2, T3, T4} - return construct_backend(cell_list, CompactVectorOfVectors{T1}, max_outer_length, + return construct_backend(CompactVectorOfVectors{T1}, max_outer_length, max_inner_length) end From 0e08ad541fce1212d883c69bfb5524314b05d292 Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Thu, 12 Jun 2025 21:49:00 +0800 Subject: [PATCH 18/21] Fix calls to `construct_backend()`. --- src/cell_lists/full_grid.jl | 4 ++-- src/cell_lists/spatial_hashing.jl | 2 +- src/vector_of_vectors.jl | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/cell_lists/full_grid.jl b/src/cell_lists/full_grid.jl index bb16166e..20e8de3e 100644 --- a/src/cell_lists/full_grid.jl +++ b/src/cell_lists/full_grid.jl @@ -63,13 +63,13 @@ function FullGridCellList(; min_corner, max_corner, if search_radius < eps() # Create an empty "template" cell list to be used with `copy_cell_list` - cells = construct_backend(FullGridCellList, backend, 0, max_points_per_cell) + cells = construct_backend(backend, 0, max_points_per_cell) linear_indices = LinearIndices(ntuple(_ -> 0, length(min_corner))) else n_cells_per_dimension = ceil.(Int, (max_corner .- min_corner) ./ search_radius) linear_indices = LinearIndices(Tuple(n_cells_per_dimension)) - cells = construct_backend(FullGridCellList, backend, prod(n_cells_per_dimension), + cells = construct_backend(backend, prod(n_cells_per_dimension), max_points_per_cell) end diff --git a/src/cell_lists/spatial_hashing.jl b/src/cell_lists/spatial_hashing.jl index 77db815f..07d9f75b 100644 --- a/src/cell_lists/spatial_hashing.jl +++ b/src/cell_lists/spatial_hashing.jl @@ -54,7 +54,7 @@ end function SpatialHashingCellList{NDIMS}(list_size, backend = DynamicVectorOfVectors{Int32}, max_points_per_cell = 100) where {NDIMS} - cells = construct_backend(SpatialHashingCellList, backend, list_size, + cells = construct_backend(backend, list_size, max_points_per_cell) collisions = [false for _ in 1:list_size] coords = [ntuple(_ -> typemin(Int), NDIMS) for _ in 1:list_size] diff --git a/src/vector_of_vectors.jl b/src/vector_of_vectors.jl index d727ba05..1cc64d6e 100644 --- a/src/vector_of_vectors.jl +++ b/src/vector_of_vectors.jl @@ -203,13 +203,11 @@ end @inline function update!(vov::CompactVectorOfVectors, f) (; values, first_bin_index, n_bins) = vov + # TODO figure out how to do that fast and on the GPU sort!(values, by = f) # TODO figure out how to do that fast and on the GPU - - # n_particles_per_cell = [count(x -> f(x) == j, values) for j in 1:n_bins[]] - n_particles_per_cell = zeros(n_bins[]) for val in values n_particles_per_cell[f(val)] += 1 From a26631770a7ab82ced7ed4d99c4009b78cb7da03 Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Thu, 12 Jun 2025 22:02:56 +0800 Subject: [PATCH 19/21] Restore tests for DynamicVectorOfVectors. --- test/neighborhood_search.jl | 1 + test/vector_of_vectors.jl | 122 +++++++++++++++++++++++++++++++----- 2 files changed, 106 insertions(+), 17 deletions(-) diff --git a/test/neighborhood_search.jl b/test/neighborhood_search.jl index eb186a18..82f17311 100644 --- a/test/neighborhood_search.jl +++ b/test/neighborhood_search.jl @@ -132,6 +132,7 @@ end end end + @testset verbose=true "Compare Against `TrivialNeighborhoodSearch`" begin cloud_sizes = [ (10, 11), diff --git a/test/vector_of_vectors.jl b/test/vector_of_vectors.jl index 5252c5e4..3b4cbe60 100644 --- a/test/vector_of_vectors.jl +++ b/test/vector_of_vectors.jl @@ -1,20 +1,108 @@ -@testset verbose=true "`StaticVectorOfVectors`" begin - n_bins = 3 - values = [2, 3, 5, 1, 4] - vov = PointNeighbors.CompactVectorOfVectors{Int}(n_bins = n_bins) - resize!(vov.values, length(values)) - vov.values .= eachindex(values) - - # Fill values and assign bins - f(x) = x % n_bins + 1 - PointNeighbors.update!(vov, f) - - # Test bin sizes - for i in 1:n_bins - bin = vov[i] - @test all(f(x) == i for x in bin) +@testset verbose=true "All VectorOfVectors Datastructures" begin + @testset verbose=true "`DynamicVectorOfVectors`" begin + # Test different types by defining a function to convert to this type + types = [Int32, Float64, i -> (i, i)] + + @testset verbose=true "Eltype $(eltype(type(1)))" for type in types + ELTYPE = typeof(type(1)) + vov_ref = Vector{Vector{ELTYPE}}() + vov = PointNeighbors.DynamicVectorOfVectors{ELTYPE}(max_outer_length = 20, + max_inner_length = 4) + + # Test internal size + @test size(vov.backend) == (4, 20) + + function verify(vov, vov_ref) + @test length(vov) == length(vov_ref) + @test eachindex(vov) == eachindex(vov_ref) + @test axes(vov) == axes(vov_ref) + + @test_throws BoundsError vov[0] + @test_throws BoundsError vov[length(vov) + 1] + + for i in eachindex(vov_ref) + @test vov[i] == vov_ref[i] + end + end + + # Initial check + verify(vov, vov_ref) + + # First `push!` + push!(vov_ref, type.([1, 2, 3])) + push!(vov, type.([1, 2, 3])) + + verify(vov, vov_ref) + + # `push!` multiple items + push!(vov_ref, type.([4]), type.([5, 6, 7, 8])) + push!(vov, type.([4]), type.([5, 6, 7, 8])) + + verify(vov, vov_ref) + + # `push!` to an inner vector + push!(vov_ref[1], type(12)) + PointNeighbors.pushat!(vov, 1, type(12)) + + # `push!` overflow + error_ = ErrorException("cell list is full. Use a larger `max_points_per_cell`.") + @test_throws error_ PointNeighbors.pushat!(vov, 1, type(13)) + + verify(vov, vov_ref) + + # Delete entry of inner vector. Note that this changes the order of the elements. + deleteat!(vov_ref[3], 2) + PointNeighbors.deleteatat!(vov, 3, 2) + + @test vov_ref[3] == type.([5, 7, 8]) + @test vov[3] == type.([5, 8, 7]) + + # Delete second to last entry + deleteat!(vov_ref[3], 2) + PointNeighbors.deleteatat!(vov, 3, 2) + + @test vov_ref[3] == type.([5, 8]) + @test vov[3] == type.([5, 7]) + + # Delete last entry + deleteat!(vov_ref[3], 2) + PointNeighbors.deleteatat!(vov, 3, 2) + + # Now they are identical again + verify(vov, vov_ref) + + # Delete the remaining entry of this vector + deleteat!(vov_ref[3], 1) + PointNeighbors.deleteatat!(vov, 3, 1) + + verify(vov, vov_ref) + + # `empty!` + empty!(vov_ref) + empty!(vov) + + verify(vov, vov_ref) + end end - # Test that all values are present - @test sort(vcat([collect(vov[i]) for i in 1:n_bins]...)) == sort(vov.values) + @testset verbose=true "`CompactVectorOfVectors`" begin + n_bins = 3 + values = [2, 3, 5, 1, 4] + vov = PointNeighbors.CompactVectorOfVectors{Int}(n_bins = n_bins) + resize!(vov.values, length(values)) + vov.values .= eachindex(values) + + # Fill values and assign bins + f(x) = x % n_bins + 1 + PointNeighbors.update!(vov, f) + + # Test bin sizes + for i in 1:n_bins + bin = vov[i] + @test all(f(x) == i for x in bin) + end + + # Test that all values are present + @test sort(vcat([collect(vov[i]) for i in 1:n_bins]...)) == sort(vov.values) + end end From 3510f7b3d3d9fbdf68e21c0f2dc905427c53ba1e Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Mon, 27 Oct 2025 17:06:35 +0100 Subject: [PATCH 20/21] Formatting. --- Project.toml | 25 +++++++++++++++++++++++++ test/neighborhood_search.jl | 2 +- test/vector_of_vectors.jl | 2 +- 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 Project.toml diff --git a/Project.toml b/Project.toml new file mode 100644 index 00000000..0b88e2c5 --- /dev/null +++ b/Project.toml @@ -0,0 +1,25 @@ +name = "PointNeighbors" +uuid = "1c4d5385-0a27-49de-8e2c-43b175c8985c" +authors = ["Erik Faulhaber "] +version = "0.6.3" + +[deps] +Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" +Atomix = "a9b6321e-bd34-4604-b9c9-b65b8de01458" +GPUArraysCore = "46192b85-c4d5-4398-a991-12ede77f4527" +KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Polyester = "f517fe37-dbe3-4b94-8317-1923a5111588" +Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + +[compat] +Adapt = "4" +Atomix = "1" +GPUArraysCore = "0.2" +KernelAbstractions = "0.9" +LinearAlgebra = "1" +Polyester = "0.7.5" +Reexport = "1" +StaticArrays = "1" +julia = "1.10" diff --git a/test/neighborhood_search.jl b/test/neighborhood_search.jl index 74e258db..8463a417 100644 --- a/test/neighborhood_search.jl +++ b/test/neighborhood_search.jl @@ -132,7 +132,7 @@ end end end - + @testset verbose=true "Compare Against `TrivialNeighborhoodSearch`" begin cloud_sizes = [ (10, 11), diff --git a/test/vector_of_vectors.jl b/test/vector_of_vectors.jl index 3b4cbe60..28ae58a3 100644 --- a/test/vector_of_vectors.jl +++ b/test/vector_of_vectors.jl @@ -1,4 +1,4 @@ -@testset verbose=true "All VectorOfVectors Datastructures" begin +@testset verbose=true "All VectorOfVectors Datastructures" begin @testset verbose=true "`DynamicVectorOfVectors`" begin # Test different types by defining a function to convert to this type types = [Int32, Float64, i -> (i, i)] From 5bb07c130a1e9d00da3c68190421ecd087b46ce8 Mon Sep 17 00:00:00 2001 From: RubberLanding Date: Tue, 28 Oct 2025 16:17:25 +0100 Subject: [PATCH 21/21] Minor change. --- src/cell_lists/spatial_hashing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cell_lists/spatial_hashing.jl b/src/cell_lists/spatial_hashing.jl index 1bb1f7f0..0db87130 100644 --- a/src/cell_lists/spatial_hashing.jl +++ b/src/cell_lists/spatial_hashing.jl @@ -45,7 +45,7 @@ function supported_update_strategies(::SpatialHashingCellList{<:Any, end function supported_update_strategies(::SpatialHashingCellList) - return (SerialUpdate;) + return (SerialUpdate,) end function SpatialHashingCellList{NDIMS}(; list_size,