BalenCPP
BalenCPP

Reputation: 301

What is an elegant way to animate a Makie.jl plot recipe for Agents.jl in Julia?

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

Answers (1)

BalenCPP
BalenCPP

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

Related Questions