ABM and CellListMap#
m3g/CellListMap.jl can quickly calculate partical pairs interactions within a cutoff.
Modeling the particle repulsion:
\[\begin{split}
\begin{align}
U(r) &= k_i k_j (r^2 - (r_i - r_j)^2)^2, \quad \text{for} r ≤ (r_i + r_j) \\
U(r) &= 0, \quad \text{otherwise}
\end{align}
\end{split}\]
using Agents
using CellListMap
using CairoMakie
CairoMakie.activate!(px_per_unit = 1.0)
Define particle agents
@agent struct Particle(ContinuousAgent{2,Float64})
r::Float64 ## radius
k::Float64 ## repulsion force constant
mass::Float64
end
Particle(; vel, r, k, mass) = (vel, r, k, mass)
Main.var"##240".Particle
Building the model
function initialize_bouncing(;
number_of_particles=10_000,
sides=SVector(500.0, 500.0),
dt=0.001,
max_radius=10.0,
parallel=true
)
# initial random positions
positions = [sides .* rand(SVector{2,Float64}) for _ in 1:number_of_particles]
# We will use CellListMap to compute forces, with similar structure as the positions
forces = similar(positions)
# Space for the agents
space2d = ContinuousSpace(sides; periodic=true)
# Initialize CellListMap particle system
system = ParticleSystem(
positions=positions,
unitcell=sides,
cutoff=2 * max_radius,
output=forces,
output_name=:forces, ## allows the system.forces alias for clarity
parallel=parallel,
)
# define the ABModel properties
# The system field contains the data required for CellListMap.jl
properties = (;dt, number_of_particles, system)
model = StandardABM(Particle,
space2d;
agent_step!,
model_step!,
agents_first = false,
properties=properties
)
# Create active agents
for id in 1:number_of_particles
pos = positions[id]
prop_particle = Particle(
r = (0.5 + 0.9 * rand()) * max_radius,
k = 10 + 20 * rand(), ## random force constants
mass = 10.0 + 100 * rand(), ## random masses
vel = 100 * randn(SVector{2}) ## initial velocities)
)
add_agent!(pos, Particle, model, prop_particle...)
end
return model
end
initialize_bouncing (generic function with 1 method)
Computing the repulsion force It must follow CellListMap API and return a array for forces acting upon the particles
function calc_forces!(x, y, i, j, d2, forces, model)
p_i = model[i]
p_j = model[j]
d = sqrt(d2)
if d ≤ (p_i.r + p_j.r)
dr = y - x ## x and y are minimum-image relative coordinates
fij = 2 * (p_i.k * p_j.k) * (d2 - (p_i.r + p_j.r)^2) * (dr / d)
forces[i] += fij
forces[j] -= fij
end
return forces
end
calc_forces! (generic function with 1 method)
Update the pairwise forces using CellListMap API
function model_step!(model::ABM)
map_pairwise!(
(x, y, i, j, d2, forces) -> calc_forces!(x, y, i, j, d2, forces, model),
model.system,
)
return nothing
end
model_step! (generic function with 1 method)
Update agent positions and velocity
function agent_step!(agent, model::ABM)
id = agent.id
dt = abmproperties(model).dt
force = model.system.forces[id]
acc = force / agent.mass
# Update positions and velocities
vel = agent.vel + acc * dt
x = agent.pos + vel * dt + (acc / 2) * dt^2
x = normalize_position(x, model) ## Wraps agent position
agent.vel = vel
move_agent!(agent, x, model)
# !!! Remember to update positions in the ParticleSystem
model.system.positions[id] = agent.pos
return nothing
end
agent_step! (generic function with 1 method)
Run simulation
function simulate(model=nothing; nsteps=1_000, number_of_particles=10_000)
if isnothing(model)
model = initialize_bouncing(number_of_particles=number_of_particles)
end
Agents.step!(model, nsteps)
end
simulate (generic function with 2 methods)
Test the performance
model = initialize_bouncing(number_of_particles=10_000)
@time simulate(model)
10.546340 seconds (474.15 k allocations: 30.136 MiB, 469 lock conflicts, 3.64% compilation time)
StandardABM with 10000 agents of type Particle
agents container: Dict
space: periodic continuous space with [500.0, 500.0] extent and spacing=25.0
scheduler: fastest
properties: dt, number_of_particles, system
The helper function below is adapted from Agents.abmvideo
and correctly displays animations in Jupyter notebooks
function abmvio(model;
dt = 1, framerate = 30, frames = 300, title = "", showstep = true,
figure = (size = (600, 600),), axis = NamedTuple(),
recordkwargs = (compression = 23, format ="mp4"), kwargs...
)
# title and steps
abmtime_obs = Observable(abmtime(model))
if title ≠ "" && showstep
t = lift(x -> title*", time = "*string(x), abmtime_obs)
elseif showstep
t = lift(x -> "time = "*string(x), abmtime_obs)
else
t = title
end
axis = (title = t, titlealign = :left, axis...)
# First frame
fig, ax, abmobs = abmplot(model; add_controls = false, warn_deprecation = false, figure, axis, kwargs...)
resize_to_layout!(fig)
# Animation
Makie.Record(fig; framerate, recordkwargs...) do io
for j in 1:frames-1
recordframe!(io)
Agents.step!(abmobs, dt)
abmtime_obs[] = abmtime(model)
end
recordframe!(io)
end
end
abmvio (generic function with 1 method)
Visualize
model = initialize_bouncing(number_of_particles=1000)
vio = abmvio(
model;
framerate=20, frames=200, dt=5,
title="Softly bouncing particles",
agent_size=p -> p.r,
agent_color=p -> p.k
)
vio |> display
This notebook was generated using Literate.jl.