Skip to content

Commit e962a6e

Browse files
authored
feat: implement Verlet neighbour list (#65)
**Summary** This PR implements a flavor of Verlet list. It addresses #63. The VerletList object contains for each particle in the system, an array of all its neighbours (defined as all particles within the interaction cutoff + some arbitrary parameter dr). These arrays of neighbours are updated only when a given particle moves a distance greater than dr/2, ensuring that the list of neighbours always contains all particles within the interaction cutoff radius. **Test plan** The CI was updated to check over a trajectory that the energy is identical to that obtained using different neighbour list methods. **Performances** The VerletList implementation was compared to the LinkedList one, for a system of ortho-terphenyl molecules, at density 1.2 and temperature 2. In the attached plot, the simulation time is reported for different values of the parameter dr. The dashed line at the top is the timing with the LinkedList implementation. For reference, the interaction cutoff is 1.23. The timing is done once for each Verlet list, and 4 times for the LinkedList, with values ranging from 75s to 105s. There's quite a lot of variability between repeats, but we still have a ~2x speedup for this system, for 0.3 < dr < 0.6. <img width="640" height="480" alt="verlet" src="https://github.com/user-attachments/assets/a2175cb8-1746-4092-b414-528dab85a5fe" />
1 parent 4212ce9 commit e962a6e

6 files changed

Lines changed: 198 additions & 17 deletions

File tree

src/IO/IO.jl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module IO
22

33
using ..ParticlesMC: Particles, Atoms, Molecules, System
44
using ..ParticlesMC: fold_back, volume_sphere
5-
using ..ParticlesMC: EmptyList, LinkedList, CellList
5+
using ..ParticlesMC: EmptyList, LinkedList, CellList, VerletList
66
using ..ParticlesMC: Model, GeneralKG, JBB, BHHP, SoftSpheres, KobAndersen, Trimer, LennardJones
77
using Arianna
88
using Distributions, LinearAlgebra, StaticArrays, Printf
@@ -286,16 +286,17 @@ function load_chains(init_path; args=Dict(), verbose=false)
286286
if haskey(args, "list_type") && !isnothing(args["list_type"])
287287
list_type = eval(Meta.parse(args["list_type"]))
288288
end
289+
list_parameters = get(args, "list_parameters", nothing)
289290
verbose && println("Using $list_type as cell list type")
290291
# Create independent chains
291292
bool_molecule = :molecule in keys(config_dict[1])
292293
if bool_molecule
293294
initial_molecule_array = broadcast_dict(config_dict, :molecule)
294295
initial_bond_array = broadcast_dict(config_dict, :bond)
295296
#initial_btype_array = broadcast_dict(config_dict, :btype)
296-
chains = [System(initial_position_array[k], initial_species_array[k], initial_molecule_array[k], initial_density_array[k], initial_temperature_array[k], model_matrix, initial_bond_array[k], list_type=list_type) for k in eachindex(initial_position_array)]
297+
chains = [System(initial_position_array[k], initial_species_array[k], initial_molecule_array[k], initial_density_array[k], initial_temperature_array[k], model_matrix, initial_bond_array[k], list_type=list_type, list_parameters=list_parameters) for k in eachindex(initial_position_array)]
297298
else
298-
chains = [System(initial_position_array[k], initial_species_array[k], initial_density_array[k], initial_temperature_array[k], model_matrix, list_type=list_type) for k in eachindex(initial_position_array)]
299+
chains = [System(initial_position_array[k], initial_species_array[k], initial_density_array[k], initial_temperature_array[k], model_matrix, list_type=list_type, list_parameters=list_parameters) for k in eachindex(initial_position_array)]
299300
end
300301
verbose && println("$(length(chains)) chains created")
301302
return chains

src/ParticlesMC.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ end
115115
export callback_energy
116116
#export nearest_image_distance
117117
export Model, GeneralKG, JBB, BHHP, SoftSpheres, KobAndersen, Trimer
118-
export NeighbourList, LinkedList, CellList, EmptyList
118+
export NeighbourList, LinkedList, CellList, EmptyList, VerletList
119119
export Atoms, Molecules
120120
export Displacement, DiscreteSwap, MoleculeFlip
121121
export fold_back, System
@@ -153,6 +153,7 @@ ParticlesMC implemented in Comonicon.
153153
error("Configuration file '$config' does not exist in the current path.")
154154
end
155155
list_type = get(system, "list_type", "LinkedList") # optional field
156+
list_parameters = get(system, "list_parameters", nothing) # optional field
156157
bonds = get(system, "bonds", nothing)
157158

158159
# Extract simulation parameters
@@ -173,6 +174,7 @@ ParticlesMC implemented in Comonicon.
173174
"density" => [density],
174175
"model" => [model],
175176
"list_type" => list_type,
177+
"list_parameters" => list_parameters,
176178
"bonds" => bonds,
177179
))
178180
else
@@ -181,6 +183,7 @@ ParticlesMC implemented in Comonicon.
181183
"density" => [density],
182184
"model" => [model],
183185
"list_type" => list_type,
186+
"list_parameters" => list_parameters,
184187
))
185188
end
186189
algorithm_list = []

src/atoms.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@ Create an `Atoms` system, initialize its neighbour list, and compute initial ene
3737
constructs an `Atoms` instance, builds the neighbour list of type `list_type`,
3838
and computes the initial total energy (stored in `system.energy[1]`).
3939
"""
40-
function System(position, species, density::T, temperature::T, model_matrix; list_type=EmptyList) where{T<:AbstractFloat}
40+
function System(position, species, density::T, temperature::T, model_matrix; list_type=EmptyList, list_parameters=nothing) where{T<:AbstractFloat}
4141
@assert length(position) == length(species)
4242
N = length(position)
4343
d = length(Array(position)[1])
4444
energy = Vector{T}(undef, 1)
4545
box = @SVector fill(T((N / density)^(1 / d)), d)
4646
maxcut = maximum([model.rcut for model in model_matrix])
47-
neighbour_list = list_type(box, maxcut, N)
47+
neighbour_list = list_type(box, maxcut, N; list_parameters=list_parameters)
4848
species_list = isa(species[1], Integer) ? SpeciesList(species) : nothing
4949
system = Atoms(position, species, density, energy, temperature, model_matrix, N, d, box, neighbour_list, species_list)
5050
build_neighbour_list!(system)

src/molecules.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ Arguments
7373
Returns
7474
- `Molecules` instance with neighbour list built and `energy[1]` set.
7575
"""
76-
function System(position, species, molecule, density::T, temperature::T, model_matrix, bonds; molecule_species=nothing, list_type=EmptyList) where {T<:AbstractFloat}
76+
function System(position, species, molecule, density::T, temperature::T, model_matrix, bonds; molecule_species=nothing, list_type=EmptyList, list_parameters=nothing) where {T<:AbstractFloat}
7777
@assert length(position) == length(species)
7878
N = length(position)
7979
Nmol = length(unique(molecule))
@@ -83,7 +83,7 @@ function System(position, species, molecule, density::T, temperature::T, model_m
8383
box = @SVector fill(T((N / density)^(1 / d)), d)
8484
energy = zeros(T, 1)
8585
maxcut = maximum([model.rcut for model in model_matrix])
86-
neighbour_list = list_type(box, maxcut, N)
86+
neighbour_list = list_type(box, maxcut, N; list_parameters=list_parameters)
8787
system = Molecules(position, species, molecule, molecule_species, start_mol, length_mol, density, temperature, energy, model_matrix, d, N, Nmol,box, neighbour_list, bonds)
8888
build_neighbour_list!(system)
8989
local_energy = [compute_energy_particle(system, i, neighbour_list) for i in eachindex(position)]

src/neighbours.jl

Lines changed: 169 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ struct EmptyList <: NeighbourList end
1818

1919
"""Construct an `EmptyList` (ignores box, rcut, N).
2020
"""
21-
function EmptyList(box, rcut, N)
21+
function EmptyList(box, rcut, N; list_parameters=nothing)
2222
return EmptyList()
2323
end
2424

@@ -114,7 +114,7 @@ end
114114
115115
Cells are chosen so that their dimensions are at least `rcut`, and neighbour cells are precomputed.
116116
"""
117-
function CellList(box::SVector{d,T}, rcut::T, N::Int) where {d,T<:AbstractFloat}
117+
function CellList(box::SVector{d,T}, rcut::T, N::Int; list_parameters=nothing) where {d,T<:AbstractFloat}
118118

119119
# Calculate cell dimensions ensuring they're >= rcut
120120
cell_box = @. box / floor(Int, box / rcut)
@@ -233,7 +233,7 @@ end
233233

234234
"""Construct a `LinkedList` neighbour list given box, cutoff `rcut`, and number of particles `N`.
235235
"""
236-
function LinkedList(box, rcut, N)
236+
function LinkedList(box, rcut, N; list_parameters=nothing)
237237
cell = box ./ fld.(box, rcut)
238238
ncells = ntuple(a -> Int(box[a] / cell[a]), length(box))
239239
head = -ones(Int, prod(ncells))
@@ -375,3 +375,169 @@ function (linked_list::LinkedList)(system::Particles, i::Int)
375375

376376
return LinkedIterator(neighbour_cells, linked_list.head, linked_list.list)
377377
end
378+
379+
380+
# Verlet list
381+
382+
"""Verlet-list neighbour list implementation.
383+
384+
Uses a cell list to find all particles within rcut + dr.
385+
Keeps an array of neighbour ids for each particle
386+
"""
387+
struct VerletList{T<:AbstractFloat, d} <: NeighbourList
388+
cs::Vector{Int}
389+
box::SVector{d,T}
390+
ncells::NTuple{d,Int}
391+
cells::Vector{Vector{Int}} # List of particles in each cell
392+
rcut::T
393+
dr::T
394+
dr2o4::T
395+
neighbours::Vector{Vector{Int}}
396+
neighbour_cells::Vector{Vector{Int}} # List of neighbouring cells
397+
positions_at_last_update::Vector{SVector{d,T}}
398+
end
399+
400+
"""Construct a `VerletList` neighbour list given box, cutoff `rcut`, cutoff padding `dr`, and number of particles `N`.
401+
"""
402+
function VerletList(box, rcut, N; list_parameters=nothing)
403+
if list_parameters == nothing || !haskey(list_parameters, "dr")
404+
error("Verlet list must have dr as a parameter")
405+
end
406+
dr = list_parameters["dr"]
407+
cell = box ./ fld.(box, rcut + dr)
408+
ncells = ntuple(a -> Int(box[a] / cell[a]), length(box))
409+
head = -ones(Int, prod(ncells))
410+
list = zeros(Int, N)
411+
cs = zeros(Int, N)
412+
neighbour_cells = build_neighbour_cells(ncells)
413+
neighbours = [Int[] for _ in 1:N]
414+
positions_at_last_update = [zeros(SVector{length(box), typeof(box[1])}) for _ in 1:N]
415+
cells = [Int[] for _ in 1:prod(ncells)]
416+
return VerletList(cs, cell, ncells, cells, rcut, dr, (dr^2)/4, neighbours, neighbour_cells, positions_at_last_update)
417+
end
418+
419+
"""Calling a VerletList objects return an object which can be iterated upon.
420+
421+
This iteration will return the indices of the neighbours of particle i.
422+
"""
423+
function (verlet_list::VerletList)(::Particles, i::Int)
424+
return (j for j in verlet_list.neighbours[i])
425+
end
426+
427+
"""Populate `neighbour_list` (a `VerletList`) by constructing the list of neighbours for each particle.
428+
429+
These neighbours are all particles within a distance `rcut` + `dr`
430+
"""
431+
function build_neighbour_list!(system::Particles, neighbour_list::VerletList)
432+
# Populate cell list
433+
for (i, position_i) in enumerate(system)
434+
c = get_cell_index(position_i, neighbour_list)
435+
neighbour_list.cs[i] = c
436+
push!(neighbour_list.cells[c], i) # Directly append particle index
437+
end
438+
439+
# Now iterate over all pairs i,j, and see if they are within the cutoff
440+
r_cutoff2 = (neighbour_list.rcut + neighbour_list.dr)^2
441+
for (i, position_i) in enumerate(system)
442+
c = get_cell_index(position_i, neighbour_list)
443+
neighbour_cells = neighbour_list.neighbour_cells[c]
444+
# Scan the neighbourhood of cell mc (including itself)
445+
# and from there scan atoms in cell c2
446+
for c2 in neighbour_cells
447+
@inbounds for j in neighbour_list.cells[c2]
448+
# Don't double count, or self interact
449+
if j <= i
450+
continue
451+
end
452+
position_j = get_position(system, j)
453+
r2 = nearest_image_distance_squared(position_i, position_j, get_box(system))
454+
if r2 < r_cutoff2
455+
push!(neighbour_list.neighbours[i], j)
456+
push!(neighbour_list.neighbours[j], i)
457+
end
458+
end
459+
end
460+
461+
neighbour_list.positions_at_last_update[i] = copy(position_i)
462+
end
463+
464+
return nothing
465+
end
466+
467+
468+
"""Update particle `i` moving from cell `c` to cell `c2` in a `VerletList`.
469+
470+
Performs an in-place removal from the old cell and appends to the new one.
471+
"""
472+
function update_cell_list!(i, c, c2, neighbour_list::VerletList)
473+
# Remove from old cell using in-place filter
474+
filter!(x -> x != i, neighbour_list.cells[c])
475+
# Add to new cell
476+
push!(neighbour_list.cells[c2], i)
477+
# Update particle's cell index
478+
neighbour_list.cs[i] = c2
479+
return nothing
480+
end
481+
482+
"""Return the old and new cell indices for particle `i` in `neighbour_list` (VerletList).
483+
484+
Used to detect whether a particle has moved between cells.
485+
"""
486+
function old_new_cell(system::Particles, i, neighbour_list::VerletList)
487+
c = neighbour_list.cs[i]
488+
# New cell index
489+
mc2 = get_cell(get_position(system, i), neighbour_list)
490+
c2 = cell_index(neighbour_list, mc2)
491+
return c, c2
492+
end
493+
494+
"""Update Verlet neighbour list
495+
496+
The cell and list for any given particle is only update if it has moved a distance > dr/2 since the last update"""
497+
function update_neighbour_list!(system::Particles, i::Int, neighbour_list::VerletList)
498+
# Check if particle moved for more than half of dr since last update
499+
position_i = get_position(system, i)
500+
# WARNING: assumes that positions are never wrapped
501+
dx = position_i - neighbour_list.positions_at_last_update[i]
502+
if sum(abs2, dx) < neighbour_list.dr2o4
503+
return nothing
504+
end
505+
506+
# Need to recompute neighbour list for the particle
507+
# Alternatively, we could redo everything and reset all positions, unclear which one is the fastest
508+
509+
# First, remove i for all other lists
510+
@inbounds for j in neighbour_list.neighbours[i]
511+
# TODO: benchmark against filter
512+
deleteat!(neighbour_list.neighbours[j], neighbour_list.neighbours[j] .== i)
513+
end
514+
neighbour_list.neighbours[i] = Int[]
515+
516+
# Update i's cell if needed
517+
c, c2 = old_new_cell(system, i, neighbour_list)
518+
if c != c2
519+
update_cell_list!(i, c, c2, neighbour_list)
520+
end
521+
522+
# Then, recreate list for i
523+
r_cutoff2 = (neighbour_list.rcut + neighbour_list.dr)^2
524+
c = get_cell_index(position_i, neighbour_list)
525+
neighbour_cells = neighbour_list.neighbour_cells[c]
526+
for c2 in neighbour_cells
527+
@inbounds for j in neighbour_list.cells[c2]
528+
if i == j
529+
continue
530+
end
531+
position_j = get_position(system, j)
532+
r2 = nearest_image_distance_squared(position_i, position_j, get_box(system))
533+
if r2 < r_cutoff2
534+
push!(neighbour_list.neighbours[i], j)
535+
push!(neighbour_list.neighbours[j], i)
536+
end
537+
end
538+
end
539+
540+
neighbour_list.positions_at_last_update[i] = copy(position_i)
541+
542+
return nothing
543+
end

test/runtests.jl

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,28 @@ using Pkg
1414
ENV["PATH"] = ENV["PATH"] * path_sep * julia_bin
1515
@test success(`bash -c "command -v particlesmc"`)
1616
@test success(`particlesmc params.toml`)
17-
end
17+
end
1818

1919

2020
@testset "Potential energy test" begin
2121
# Test inital configuration
2222
chains_el = load_chains("config_0.exyz", args=Dict("temperature" => [0.231], "model" => ["JBB"], "list_type" => "EmptyList"))
2323
chains_ll = load_chains("config_0.lmp", args=Dict("temperature" => [0.231], "model" => ["JBB"], "list_type" => "LinkedList"))
24+
chains_vl = load_chains("config_0.lmp", args=Dict("temperature" => [0.231], "model" => ["JBB"], "list_type" => "VerletList", "list_parameters" => Dict("dr" => 0.2)))
2425
system_el = chains_el[1]
2526
system_ll = chains_ll[1]
27+
system_vl = chains_vl[1]
2628
@test system_el.N == system_ll.N
2729
@test system_el.d == system_ll.d
2830
@test system_el.temperature == system_ll.temperature
2931
@test all.(system_el.position == system_ll.position)
3032
@test all.(system_el.species == system_ll.species)
3133
energy_el = system_el.energy[1] / length(system_el)
3234
energy_ll = system_ll.energy[1] / length(system_ll)
35+
energy_vl = system_vl.energy[1] / length(system_vl)
3336
@test isapprox(energy_el, -2.676832, atol=1e-6)
3437
@test isapprox(energy_ll, -2.676832, atol=1e-6)
38+
@test isapprox(energy_vl, -2.676832, atol=1e-6)
3539

3640
# Test simulation energy
3741
M = 1
@@ -63,19 +67,28 @@ end
6367
path_el = "data/noswap/empty_list/"
6468
simulation = Simulation(chains_el, algorithm_list, steps; path=path_el, verbose=true)
6569
run!(simulation)
66-
70+
6771
## Linked List simulation
6872
chains_ll = [deepcopy(system_ll)]
6973
path_ll = "data/noswap/linked_list/"
7074
simulation = Simulation(chains_ll, algorithm_list, steps; path=path_ll, verbose=true)
7175
run!(simulation)
7276

77+
## Verlet List simulation
78+
chains_vl = [deepcopy(system_vl)]
79+
path_vl = "data/noswap/verlet_list/"
80+
simulation = Simulation(chains_vl, algorithm_list, steps; path=path_vl, verbose=true)
81+
run!(simulation)
82+
7383
## Read energy data and compare
7484
path_energy_el = joinpath(path_el, "energy.dat")
7585
path_energy_ll = joinpath(path_ll, "energy.dat")
86+
path_energy_vl = joinpath(path_vl, "energy.dat")
7687
energy_el= readdlm(path_energy_el)[:, 2]
7788
energy_ll = readdlm(path_energy_ll)[:, 2]
89+
energy_vl = readdlm(path_energy_vl)[:, 2]
7890
@test isapprox(energy_el, energy_ll, atol=1e-6)
91+
@test isapprox(energy_ll, energy_vl, atol=1e-6)
7992

8093
# SWAPS
8194
pswap = 0.8
@@ -100,7 +113,7 @@ end
100113
path_el = "data/swap/empty_list/"
101114
simulation = Simulation(chains_el, algorithm_list, steps; path=path_el, verbose=true)
102115
run!(simulation)
103-
116+
104117
## Linked List simulation
105118
chains_ll = [deepcopy(system_ll)]
106119
path_ll = "data/swap/linked_list/"
@@ -162,7 +175,7 @@ end
162175
path_el = "data/noswap/empty_list/"
163176
simulation = Simulation(chains_el, algorithm_list, steps; path=path_el, verbose=true)
164177
run!(simulation)
165-
178+
166179
## Linked List simulation
167180
chains_ll = [deepcopy(system_ll)]
168181
path_ll = "data/noswap/linked_list/"
@@ -177,5 +190,3 @@ end
177190
@test isapprox(energy_el, energy_ll, atol=1e-6)
178191

179192
end
180-
181-

0 commit comments

Comments
 (0)