Skip to content

Commit f4109dd

Browse files
efaulhabersvchb
andauthored
Add "full grid" cell list with a separate list for each cell of a rectangular domain (trixi-framework#25)
* Add `FullGridCellList` * Fix tests * Reformat * Fix copying * Use kwargs * Add docs * Fix docs * Fix typo * Fix variable names * Fix `is_correct_cell` * Avoid allocations from unnecessary `collect` * Explicitly mention that rectangular domains have to be axis-aligned * Implement suggestions --------- Co-authored-by: Sven Berger <[email protected]>
1 parent 5d4cfeb commit f4109dd

File tree

8 files changed

+239
-44
lines changed

8 files changed

+239
-44
lines changed

src/PointNeighbors.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ include("nhs_precomputed.jl")
1515

1616
export foreach_point_neighbor, foreach_neighbor
1717
export TrivialNeighborhoodSearch, GridNeighborhoodSearch, PrecomputedNeighborhoodSearch
18+
export DictionaryCellList, FullGridCellList
1819
export initialize!, update!, initialize_grid!, update_grid!
1920
export PeriodicBox, copy_neighborhood_search
2021

src/cell_lists/cell_lists.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1+
abstract type AbstractCellList end
2+
3+
# For the `DictionaryCellList`, this is a `KeySet`, which has to be `collect`ed first to be
4+
# able to be used in a threaded loop.
5+
@inline each_cell_index_threadable(cell_list::AbstractCellList) = each_cell_index(cell_list)
6+
17
include("dictionary.jl")
8+
include("full_grid.jl")

src/cell_lists/dictionary.jl

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
1-
struct DictionaryCellList{NDIMS}
1+
"""
2+
DictionaryCellList{NDIMS}()
3+
4+
A simple cell list implementation where a cell index `(i, j)` or `(i, j, k)` is mapped to a
5+
`Vector{Int}` by a `Dict`.
6+
By using a dictionary, which only stores non-empty cells, the domain is
7+
potentially infinite.
8+
9+
This implementation is very simple, but it neither uses an optimized hash function
10+
for integer tuples, nor does it use a contiguous memory layout.
11+
Consequently, this cell list is not GPU-compatible.
12+
13+
# Arguments
14+
- `NDIMS`: Number of dimensions.
15+
"""
16+
struct DictionaryCellList{NDIMS} <: AbstractCellList
217
hashtable :: Dict{NTuple{NDIMS, Int}, Vector{Int}}
318
empty_vector :: Vector{Int} # Just an empty vector (used in `eachneighbor`)
419

@@ -43,7 +58,13 @@ function delete_cell!(cell_list, cell)
4358
delete!(cell_list.hashtable, cell)
4459
end
4560

46-
@inline eachcell(cell_list::DictionaryCellList) = keys(cell_list.hashtable)
61+
@inline each_cell_index(cell_list::DictionaryCellList) = keys(cell_list.hashtable)
62+
63+
# For this cell list, this is a `KeySet`, which has to be `collect`ed first to be
64+
# able to be used in a threaded loop.
65+
@inline function each_cell_index_threadable(cell_list::DictionaryCellList)
66+
return collect(each_cell_index(cell_list))
67+
end
4768

4869
@inline function Base.getindex(cell_list::DictionaryCellList, cell)
4970
(; hashtable, empty_vector) = cell_list
@@ -52,3 +73,14 @@ end
5273
# reuse the empty vector to avoid allocations.
5374
return get(hashtable, cell, empty_vector)
5475
end
76+
77+
@inline function is_correct_cell(::DictionaryCellList, cell_coords, cell_index)
78+
return cell_coords == cell_index
79+
end
80+
81+
@inline index_type(::DictionaryCellList{NDIMS}) where {NDIMS} = NTuple{NDIMS, Int}
82+
83+
function copy_cell_list(::DictionaryCellList{NDIMS}, search_radius,
84+
periodic_box) where {NDIMS}
85+
return DictionaryCellList{NDIMS}()
86+
end

src/cell_lists/full_grid.jl

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""
2+
FullGridCellList(; min_corner, max_corner, search_radius = 0.0, periodicity = false)
3+
4+
A simple cell list implementation where each (empty or non-empty) cell of a rectangular
5+
(axis-aligned) domain is assigned a list of points.
6+
This cell list only works when all points are inside the specified domain at all times.
7+
8+
Only set `min_corner` and `max_corner` and use the default values for the other arguments
9+
to create an empty "template" cell list that can be used to create an empty "template"
10+
neighborhood search.
11+
See [`copy_neighborhood_search`](@ref) for more details.
12+
13+
# Keywords
14+
- `min_corner`: Coordinates of the domain corner in negative coordinate directions.
15+
- `max_corner`: Coordinates of the domain corner in positive coordinate directions.
16+
- `search_radius = 0.0`: Search radius of the neighborhood search, which will determine the
17+
cell size. Use the default of `0.0` to create a template (see above).
18+
- `periodicity = false`: Set to `true` when using a [`PeriodicBox`](@ref) with the
19+
neighborhood search. When using [`copy_neighborhood_search`](@ref),
20+
this option can be ignored an will be set automatically depending
21+
on the periodicity of the neighborhood search.
22+
"""
23+
struct FullGridCellList{C, LI, MC} <: AbstractCellList
24+
cells :: C
25+
linear_indices :: LI
26+
min_cell :: MC
27+
28+
function FullGridCellList{C, LI, MC}(cells, linear_indices, min_cell) where {C, LI, MC}
29+
new{C, LI, MC}(cells, linear_indices, min_cell)
30+
end
31+
end
32+
33+
function FullGridCellList(; min_corner, max_corner, search_radius = 0.0,
34+
periodicity = false)
35+
if search_radius < eps()
36+
# Create an empty "template" cell list to be used with `copy_cell_list`
37+
cells = nothing
38+
linear_indices = nothing
39+
40+
# Misuse `min_cell` to store min and max corner for copying
41+
min_cell = (min_corner, max_corner)
42+
else
43+
if periodicity
44+
# Subtract `min_corner` because that's how the grid NHS works with periodicity
45+
max_corner = max_corner .- min_corner
46+
min_corner = min_corner .- min_corner
47+
end
48+
49+
# Note that we don't shift everything so that the first cell starts at `min_corner`.
50+
# The first cell is the cell containing `min_corner`, so we need to add one layer
51+
# in order for `max_corner` to be inside a cell.
52+
n_cells_per_dimension = ceil.(Int, (max_corner .- min_corner) ./ search_radius) .+ 1
53+
linear_indices = LinearIndices(Tuple(n_cells_per_dimension))
54+
min_cell = Tuple(floor_to_int.(min_corner ./ search_radius))
55+
56+
cells = [Int32[] for _ in 1:prod(n_cells_per_dimension)]
57+
end
58+
59+
return FullGridCellList{typeof(cells), typeof(linear_indices),
60+
typeof(min_cell)}(cells, linear_indices, min_cell)
61+
end
62+
63+
function Base.empty!(cell_list::FullGridCellList)
64+
Base.empty!.(cell_list.cells)
65+
66+
return cell_list
67+
end
68+
69+
function Base.empty!(cell_list::FullGridCellList{Nothing})
70+
# This is an empty "template" cell list to be used with `copy_cell_list`
71+
throw(UndefRefError("`search_radius` is not defined for this cell list"))
72+
end
73+
74+
function push_cell!(cell_list::FullGridCellList, cell, particle)
75+
push!(cell_list[cell], particle)
76+
end
77+
78+
function push_cell!(cell_list::FullGridCellList{Nothing}, cell, particle)
79+
# This is an empty "template" cell list to be used with `copy_cell_list`
80+
throw(UndefRefError("`search_radius` is not defined for this cell list"))
81+
end
82+
83+
function deleteat_cell!(cell_list::FullGridCellList, cell, i)
84+
deleteat!(cell_list[cell], i)
85+
end
86+
87+
@inline each_cell_index(cell_list::FullGridCellList) = eachindex(cell_list.cells)
88+
89+
function each_cell_index(cell_list::FullGridCellList{Nothing})
90+
# This is an empty "template" cell list to be used with `copy_cell_list`
91+
throw(UndefRefError("`search_radius` is not defined for this cell list"))
92+
end
93+
94+
@inline function Base.getindex(cell_list::FullGridCellList, cell::Tuple)
95+
(; cells, linear_indices, min_cell) = cell_list
96+
97+
return cells[linear_indices[(cell .- min_cell .+ 1)...]]
98+
end
99+
100+
@inline function Base.getindex(cell_list::FullGridCellList, i::Integer)
101+
return cell_list.cells[i]
102+
end
103+
104+
@inline function is_correct_cell(cell_list::FullGridCellList, cell_coords, cell_index)
105+
(; linear_indices, min_cell) = cell_list
106+
107+
return linear_indices[(cell_coords .- min_cell .+ 1)...] == cell_index
108+
end
109+
110+
@inline index_type(::FullGridCellList) = Int32
111+
112+
function copy_cell_list(cell_list::FullGridCellList, search_radius, periodic_box)
113+
# Misuse `min_cell` to store min and max corner for copying
114+
min_corner, max_corner = cell_list.min_cell
115+
116+
return FullGridCellList(; min_corner, max_corner, search_radius,
117+
periodicity = !isnothing(periodic_box))
118+
end

src/neighborhood_search.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ end
7575
"""
7676
PeriodicBox(; min_corner, max_corner)
7777
78-
Define a rectangular periodic domain.
78+
Define a rectangular (axis-aligned) periodic domain.
7979
8080
# Keywords
8181
- `min_corner`: Coordinates of the domain corner in negative coordinate directions.

src/nhs_grid.jl

Lines changed: 36 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
@doc raw"""
22
GridNeighborhoodSearch{NDIMS}(; search_radius = 0.0, n_points = 0,
3-
periodic_box = nothing, threaded_update = true)
3+
periodic_box = nothing,
4+
cell_list = DictionaryCellList{NDIMS}(),
5+
threaded_update = true)
46
57
Simple grid-based neighborhood search with uniform search radius.
68
The domain is divided into a regular grid.
@@ -32,6 +34,8 @@ since not sorting makes our implementation a lot faster (although less paralleli
3234
with [`copy_neighborhood_search`](@ref).
3335
- `periodic_box = nothing`: In order to use a (rectangular) periodic domain, pass a
3436
[`PeriodicBox`](@ref).
37+
- `cell_list`: The cell list that maps a cell index to a list of points inside
38+
the cell. By default, a [`DictionaryCellList`](@ref) is used.
3539
- `threaded_update = true`: Can be used to deactivate thread parallelization in the
3640
neighborhood search update. This can be one of the largest
3741
sources of variations between simulations with different
@@ -47,23 +51,23 @@ since not sorting makes our implementation a lot faster (although less paralleli
4751
In: Computer Graphics Forum 30.1 (2011), pages 99–112.
4852
[doi: 10.1111/J.1467-8659.2010.01832.X](https://doi.org/10.1111/J.1467-8659.2010.01832.X)
4953
"""
50-
struct GridNeighborhoodSearch{NDIMS, ELTYPE, CL, PB} <: AbstractNeighborhoodSearch
54+
struct GridNeighborhoodSearch{NDIMS, ELTYPE, CL, CB, PB} <: AbstractNeighborhoodSearch
5155
cell_list :: CL
5256
search_radius :: ELTYPE
5357
periodic_box :: PB
5458
n_cells :: NTuple{NDIMS, Int} # Required to calculate periodic cell index
5559
cell_size :: NTuple{NDIMS, ELTYPE} # Required to calculate cell index
56-
cell_buffer :: Array{NTuple{NDIMS, Int}, 2} # Multithreaded buffer for `update!`
60+
cell_buffer :: CB # Multithreaded buffer for `update!`
5761
cell_buffer_indices :: Vector{Int} # Store which entries of `cell_buffer` are initialized
5862
threaded_update :: Bool
5963

6064
function GridNeighborhoodSearch{NDIMS}(; search_radius = 0.0, n_points = 0,
6165
periodic_box = nothing,
66+
cell_list = DictionaryCellList{NDIMS}(),
6267
threaded_update = true) where {NDIMS}
6368
ELTYPE = typeof(search_radius)
64-
cell_list = DictionaryCellList{NDIMS}()
6569

66-
cell_buffer = Array{NTuple{NDIMS, Int}, 2}(undef, n_points, Threads.nthreads())
70+
cell_buffer = Array{index_type(cell_list), 2}(undef, n_points, Threads.nthreads())
6771
cell_buffer_indices = zeros(Int, Threads.nthreads())
6872

6973
if search_radius < eps() || isnothing(periodic_box)
@@ -86,7 +90,7 @@ struct GridNeighborhoodSearch{NDIMS, ELTYPE, CL, PB} <: AbstractNeighborhoodSear
8690
end
8791
end
8892

89-
new{NDIMS, ELTYPE, typeof(cell_list),
93+
new{NDIMS, ELTYPE, typeof(cell_list), typeof(cell_buffer),
9094
typeof(periodic_box)}(cell_list, search_radius, periodic_box, n_cells,
9195
cell_size, cell_buffer, cell_buffer_indices,
9296
threaded_update)
@@ -156,13 +160,15 @@ function update_grid!(neighborhood_search::GridNeighborhoodSearch, coords_fun)
156160
for thread in 1:Threads.nthreads()
157161
# Only the entries `1:cell_buffer_indices[thread]` are initialized for `thread`.
158162
for i in 1:cell_buffer_indices[thread]
159-
cell = cell_buffer[i, thread]
160-
points = cell_list[cell]
163+
cell_index = cell_buffer[i, thread]
164+
points = cell_list[cell_index]
161165

162166
# Find all points whose coordinates do not match this cell
163167
moved_point_indices = (i for i in eachindex(points)
164-
if cell_coords(coords_fun(points[i]),
165-
neighborhood_search) != cell)
168+
if !is_correct_cell(cell_list,
169+
cell_coords(coords_fun(points[i]),
170+
neighborhood_search),
171+
cell_index))
166172

167173
# Add moved points to new cell
168174
for i in moved_point_indices
@@ -174,7 +180,7 @@ function update_grid!(neighborhood_search::GridNeighborhoodSearch, coords_fun)
174180
end
175181

176182
# Remove moved points from this cell
177-
deleteat_cell!(cell_list, cell, moved_point_indices)
183+
deleteat_cell!(cell_list, cell_index, moved_point_indices)
178184
end
179185
end
180186

@@ -184,33 +190,38 @@ end
184190
@inline function mark_changed_cell!(neighborhood_search, cell_list, coords_fun,
185191
threaded_update::Val{true})
186192
# `collect` the keyset to be able to loop over it with `@threaded`
187-
@threaded for cell in collect(eachcell(cell_list))
188-
mark_changed_cell!(neighborhood_search, cell, coords_fun)
193+
@threaded for cell_index in each_cell_index_threadable(cell_list)
194+
mark_changed_cell!(neighborhood_search, cell_index, coords_fun)
189195
end
190196
end
191197

192198
@inline function mark_changed_cell!(neighborhood_search, cell_list, coords_fun,
193199
threaded_update::Val{false})
194-
for cell in eachcell(cell_list)
195-
mark_changed_cell!(neighborhood_search, cell, coords_fun)
200+
for cell_index in each_cell_index(cell_list)
201+
mark_changed_cell!(neighborhood_search, cell_index, coords_fun)
196202
end
197203
end
198204

199205
# Use this function barrier and unpack inside to avoid passing closures to Polyester.jl
200206
# with `@batch` (`@threaded`).
201207
# Otherwise, `@threaded` does not work here with Julia ARM on macOS.
202208
# See https://github.com/JuliaSIMD/Polyester.jl/issues/88.
203-
@inline function mark_changed_cell!(neighborhood_search, cell, coords_fun)
209+
@inline function mark_changed_cell!(neighborhood_search, cell_index, coords_fun)
204210
(; cell_list, cell_buffer, cell_buffer_indices) = neighborhood_search
205211

206-
for point in cell_list[cell]
207-
if cell_coords(coords_fun(point), neighborhood_search) != cell
212+
for point in cell_list[cell_index]
213+
cell = cell_coords(coords_fun(point), neighborhood_search)
214+
215+
# `cell` is a tuple, `cell_index` is the linear index used internally by the
216+
# cell list to store cells inside `cell`.
217+
# These can be identical (see `DictionaryCellList`).
218+
if !is_correct_cell(cell_list, cell, cell_index)
208219
# Mark this cell and continue with the next one.
209220
#
210221
# `cell_buffer` is preallocated,
211222
# but only the entries 1:i are used for this thread.
212223
i = cell_buffer_indices[Threads.threadid()] += 1
213-
cell_buffer[i, Threads.threadid()] = cell
224+
cell_buffer[i, Threads.threadid()] = cell_index
214225
break
215226
end
216227
end
@@ -290,28 +301,12 @@ end
290301
return Tuple(floor_to_int.(offset_coords ./ cell_size))
291302
end
292303

293-
# When points end up with coordinates so big that the cell coordinates
294-
# exceed the range of Int, then `floor(Int, i)` will fail with an InexactError.
295-
# In this case, we can just use typemax(Int), since we can assume that points
296-
# that far away will not interact with anything, anyway.
297-
# This usually indicates an instability, but we don't want the simulation to crash,
298-
# since adaptive time integration methods may detect the instability and reject the
299-
# time step.
300-
# If we threw an error here, we would prevent the time integration method from
301-
# retrying with a smaller time step, and we would thus crash perfectly fine simulations.
302-
@inline function floor_to_int(i)
303-
if isnan(i) || i > typemax(Int)
304-
return typemax(Int)
305-
elseif i < typemin(Int)
306-
return typemin(Int)
307-
end
308-
309-
return floor(Int, i)
310-
end
311-
312304
function copy_neighborhood_search(nhs::GridNeighborhoodSearch, search_radius, n_points;
313305
eachpoint = 1:n_points)
314-
return GridNeighborhoodSearch{ndims(nhs)}(; search_radius, n_points,
315-
periodic_box = nhs.periodic_box,
316-
threaded_update = nhs.threaded_update)
306+
(; periodic_box, threaded_update) = nhs
307+
308+
cell_list = copy_cell_list(nhs.cell_list, search_radius, periodic_box)
309+
310+
return GridNeighborhoodSearch{ndims(nhs)}(; search_radius, n_points, periodic_box,
311+
cell_list, threaded_update)
317312
end

src/util.jl

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,25 @@
33
return SVector(ntuple(@inline(dim->A[dim, i]), NDIMS))
44
end
55

6+
# When particles end up with coordinates so big that the cell coordinates
7+
# exceed the range of Int, then `floor(Int, i)` will fail with an InexactError.
8+
# In this case, we can just use typemax(Int), since we can assume that particles
9+
# that far away will not interact with anything, anyway.
10+
# This usually indicates an instability, but we don't want the simulation to crash,
11+
# since adaptive time integration methods may detect the instability and reject the
12+
# time step.
13+
# If we threw an error here, we would prevent the time integration method from
14+
# retrying with a smaller time step, and we would thus crash perfectly fine simulations.
15+
@inline function floor_to_int(i)
16+
if isnan(i) || i > typemax(Int)
17+
return typemax(Int)
18+
elseif i < typemin(Int)
19+
return typemin(Int)
20+
end
21+
22+
return floor(Int, i)
23+
end
24+
625
"""
726
@threaded for ... end
827

0 commit comments

Comments
 (0)