Reputation: 301
Let's say we've got the following Agents.jl + Makie.jl workflow:
using Agents, Random, AgentsPlots, Makie, Observables
mutable struct BallAgent <: AbstractAgent
id::Int
pos::Tuple{Float64, Float64}
vel::Tuple{Float64, Float64}
mass::Float64
end
function ball_model(; speed = 0.002)
space2d = ContinuousSpace(2; periodic = true, extend = (1, 1))
model = AgentBasedModel(BallAgent, space2d, properties = Dict(:dt => 1e0, :i => Observable(0)))
Random.seed!(1001)
for ind ∈ 1:500
pos = Tuple(rand(Float64, 2))
vel = (sincos(rand(Float64) * 2π) |> reverse) .* speed
add_agent!(pos, model, vel, 1e0)
end
index!(model)
return model
end
agent_step!(agent::BallAgent, model) = move_agent!(agent, model, model.dt)
function AbstractPlotting.:plot!(scene::AbstractPlotting.Plot(AgentBasedModel))
ab_model = scene[1][]
position = Observable([a.pos for a ∈ allagents(ab_model)])
on(ab_model.i) do i
position[] = [a.pos for a ∈ allagents(ab_model)]
end
scatter!(scene, position, markersize = 0.01)
end
function create_animation()
model = ball_model()
scene = plot(model)
display(AbstractPlotting.PlotDisplay(), scene)
for i ∈ 1:600
Agents.step!(model, agent_step!, 1)
model.i[] = i
sleep(1/60)
end
end
Now since AgentsPlot.jl doesn't support Makie, I have to make a recipe for it and currently the way I update the plot is by registering a callback that updates the positions observable, which gets passed to scatter.
The thing is I am registering that callback on an Int observable, which is attached to the AgentBasedModel, that is created specifically for this. It just seems like an ugly way to go about this.
model = AgentBasedModel(BallAgent, space2d, properties = Dict(:dt => 1e0, :i => Observable(0)))
i is the observable which we attach the callback to like this:
on(ab_model.i) do i
position[] = [a.pos for a ∈ allagents(ab_model)]
end
and I have to update it here so the callback gets called like so:
for i ∈ 1:600
model.i[] = i
Upvotes: 4
Views: 371
Reputation: 301
One way to make it more elegant is to add a callback on the model itself and then use Observables.notify!(model)
to fire the callback(s). This way you wouldn't need another variable to keep track of but I still feel like it can be made more elegant.
using Agents, Random, AgentsPlots, Makie, Observables
mutable struct BallAgent <: AbstractAgent
id::Int
pos::Tuple{Float64, Float64}
vel::Tuple{Float64, Float64}
mass::Float64
end
function ball_model(; speed = 0.002)
space2d = ContinuousSpace(2; periodic = true, extend = (1, 1))
model = AgentBasedModel(BallAgent, space2d, properties = Dict(:dt => 1e0))
Random.seed!(1001)
for ind ∈ 1:500
pos = Tuple(rand(Float64, 2))
vel = (sincos(rand(Float64) * 2π) |> reverse) .* speed
add_agent!(pos, model, vel, 1e0)
end
index!(model)
return model
end
agent_step!(agent::BallAgent, model) = move_agent!(agent, model, model.dt)
function AbstractPlotting.:plot!(scene::AbstractPlotting.Plot(AgentBasedModel))
model = scene[1]
position = Observable([a.pos for a ∈ allagents(model[])])
on(model) do _model
position[] = [a.pos for a ∈ allagents(_model)]
end
scatter!(scene, position, markersize = 0.01)
end
function create_animation()
model = Observable(ball_model())
scene = plot(model)
display(AbstractPlotting.PlotDisplay(), scene)
for i ∈ 1:600
Agents.step!(model[], agent_step!, 1)
Observables.notify!(model)
sleep(1/60)
end
end
Edit: Observables.notify!(model)
simply does model[] = model[]
Upvotes: 2