Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

abmplot fails when 1) number of agents changes over time and 2) custom agent_size function is used #1035

Open
markusgumbel opened this issue May 14, 2024 · 20 comments
Labels
bug Something isn't working external-dependecy problem in a dependency package plotting

Comments

@markusgumbel
Copy link

markusgumbel commented May 14, 2024

Describe the bug

I have a simulation where I remove agents by chance (by remove_agent!). I also define a custom function (p_size) for an agent's size provided to abmplot. When I run the simulation (see MWE below), an exception is thrown:

Error in callback:
DimensionMismatch: arrays could not be broadcast to a common size; got a dimension with lengths 100 and 52

See also full stack trace at the end.

What am I doing wrong?

PS: Thank you very much for the Agents.jl package! I am really happy to use it.

Minimal Working Example

Here's the example. Please note that the example works when the custom agent_size function p_size is not used -- see comments at the end of the example.

using Agents, GLMakie

@agent struct Particle(ContinuousAgent{2,Float64}) end

function build_model()
    space = ContinuousSpace((1, 1))
    model = StandardABM(Particle, space; agent_step! = particle_step!)
    for i = 1:100
        pos = Tuple(rand(2))
        vel = Tuple(rand(2))
        add_agent!(Particle(i, pos, vel), model)
    end
    return model
end

function particle_step!(p, model)
    randomwalk!(p, model, 1.0)
    if rand() < 0.5
        remove_agent!(p, model) # Number of agents changes here.
    end
end

p_size(p::Particle) = 10 # Function for an agent's size.

model = build_model()

GLMakie.activate!(visible = true)
figure, ax, abmobs = abmplot(model; add_controls = true, agent_size = p_size) # does not work
# figure, ax, abmobs = abmplot(model; add_controls = true, agent_size = 10) # works
resize!(figure, 1200, 800)
wait(display(figure))

Agents.jl version

Agents v6.0.12 with Julia 1.10.2 and Linux Ubuntu 22.04.

Full stack trace

Error in callback:
DimensionMismatch: arrays could not be broadcast to a common size; got a dimension with lengths 100 and 52
Stacktrace:
  [1] _bcs1
    @ ./broadcast.jl:555 [inlined]
  [2] _bcs
    @ ./broadcast.jl:549 [inlined]
  [3] broadcast_shape
    @ ./broadcast.jl:543 [inlined]
  [4] combine_axes
    @ ./broadcast.jl:524 [inlined]
  [5] combine_axes (repeats 2 times)
    @ ./broadcast.jl:523 [inlined]
  [6] instantiate
    @ ./broadcast.jl:306 [inlined]
  [7] materialize
    @ ./broadcast.jl:903 [inlined]
  [8] offset_bezierpath(atlas::Makie.TextureAtlas, bp::BezierPath, scale::Vector{Vec{2, Float32}}, offset::Vector{Vec{2, Float32}})
    @ Makie ~/.julia/packages/Makie/ND0gA/src/utilities/texture_atlas.jl:537
  [9] offset_marker(atlas::Makie.TextureAtlas, marker::BezierPath, font::FreeTypeAbstraction.FTFont, markersize::Vector{Vec{2, Float32}}, markeroffset::Vector{Vec{2, Float32}})
    @ Makie ~/.julia/packages/Makie/ND0gA/src/utilities/texture_atlas.jl:541
 [10] invokelatest(::Any, ::Any, ::Vararg{Any}; kwargs::@Kwargs{})
    @ Base ./essentials.jl:892
 [11] invokelatest(::Any, ::Any, ::Vararg{Any})
    @ Base ./essentials.jl:889
 [12] (::Observables.MapCallback)(value::Any)
    @ Observables ~/.julia/packages/Observables/YdEbO/src/Observables.jl:436
 [13] #invokelatest#2
    @ ./essentials.jl:892 [inlined]
 [14] invokelatest
    @ ./essentials.jl:889 [inlined]
 [15] notify
    @ ~/.julia/packages/Observables/YdEbO/src/Observables.jl:206 [inlined]
 [16] setindex!(observable::Observable, val::Any)
    @ Observables ~/.julia/packages/Observables/YdEbO/src/Observables.jl:123
--- the last 5 lines are repeated 1 more time ---
 [22] (::Observables.SetindexCallback)(x::Any)
    @ Observables ~/.julia/packages/Observables/YdEbO/src/Observables.jl:148
--- the last 10 lines are repeated 1 more time ---
 [33] #invokelatest#2
    @ ./essentials.jl:892 [inlined]
 [34] invokelatest
    @ ./essentials.jl:889 [inlined]
 [35] notify
    @ ~/.julia/packages/Observables/YdEbO/src/Observables.jl:206 [inlined]
 [36] setindex!(observable::Observable, val::Any)
    @ Observables ~/.julia/packages/Observables/YdEbO/src/Observables.jl:123
 [37] (::Observables.MapCallback)(value::Any)
    @ Observables ~/.julia/packages/Observables/YdEbO/src/Observables.jl:436
 [38] #invokelatest#2
    @ ./essentials.jl:892 [inlined]
 [39] invokelatest
    @ ./essentials.jl:889 [inlined]
 [40] notify
    @ ~/.julia/packages/Observables/YdEbO/src/Observables.jl:206 [inlined]
 [41] step!(abmobs::ABMObservable{Observable{StandardABM{ContinuousSpace{2, true, Float64, typeof(Agents.no_vel_update)}, Particle, Dict{Int64, Particle}, Tuple{DataType}, typeof(particle_step!), typeof(dummystep), typeof(Agents.Schedulers.fastest), Nothing, Random.TaskLocalRNG}}, Nothing, Nothing, Nothing, Nothing, Bool, Observable{Int64}, Observable{Tuple{Base.RefValue{Int64}, Vector{Int64}}}}, t::Int64)
    @ AgentsVisualizations ~/.julia/packages/Agents/0NJQH/ext/AgentsVisualizations/src/model_observable.jl:38
 [42] (::AgentsVisualizations.var"#48#55"{ABMObservable{Observable{StandardABM{ContinuousSpace{2, true, Float64, typeof(Agents.no_vel_update)}, Particle, Dict{Int64, Particle}, Tuple{DataType}, typeof(particle_step!), typeof(dummystep), typeof(Agents.Schedulers.fastest), Nothing, Random.TaskLocalRNG}}, Nothing, Nothing, Nothing, Nothing, Bool, Observable{Int64}, Observable{Tuple{Base.RefValue{Int64}, Vector{Int64}}}}, Observable{Any}})(c::Int64)
    @ AgentsVisualizations ~/.julia/packages/Agents/0NJQH/ext/AgentsVisualizations/src/interaction.jl:47
 [43] #invokelatest#2
    @ ./essentials.jl:892 [inlined]
 [44] invokelatest
    @ ./essentials.jl:889 [inlined]
 [45] notify
    @ ~/.julia/packages/Observables/YdEbO/src/Observables.jl:206 [inlined]
 [46] setindex!(observable::Observable, val::Any)
    @ Observables ~/.julia/packages/Observables/YdEbO/src/Observables.jl:123
 [47] (::Makie.var"#1973#1983"{Button, Observable{Symbol}})(::MouseEvent)
    @ Makie ~/.julia/packages/Makie/ND0gA/src/makielayout/blocks/button.jl:68
 [48] (::Makie.var"#1371#1372"{Makie.var"#1973#1983"{Button, Observable{Symbol}}})(event::MouseEvent)
    @ Makie ~/.julia/packages/Makie/ND0gA/src/makielayout/mousestatemachine.jl:94
 [49] #invokelatest#2
    @ ./essentials.jl:892 [inlined]
 [50] invokelatest
    @ ./essentials.jl:889 [inlined]
 [51] notify
    @ ~/.julia/packages/Observables/YdEbO/src/Observables.jl:206 [inlined]
 [52] setindex!
    @ ~/.julia/packages/Observables/YdEbO/src/Observables.jl:123 [inlined]
 [53] (::Makie.var"#1441#1443"{Scene, Base.RefValue{Bool}, Base.RefValue{Union{Nothing, Makie.Mouse.Button}}, Base.RefValue{Float64}, Base.RefValue{Float64}, Base.RefValue{Bool}, Base.RefValue{Bool}, Base.RefValue{Union{Nothing, Makie.Mouse.Button}}, Base.RefValue{Bool}, Base.RefValue{Point{2, Float32}}, Base.RefValue{Point{2, Float32}}, Base.RefValue{Makie.Mouse.Action}, Observable{MouseEvent}, Float64, Module})(event::Makie.MouseButtonEvent)
    @ Makie ~/.julia/packages/Makie/ND0gA/src/makielayout/mousestatemachine.jl:299
 [54] #invokelatest#2
    @ ./essentials.jl:892 [inlined]
 [55] invokelatest
    @ ./essentials.jl:889 [inlined]
 [56] notify
    @ ~/.julia/packages/Observables/YdEbO/src/Observables.jl:206 [inlined]
 [57] setindex!
    @ ~/.julia/packages/Observables/YdEbO/src/Observables.jl:123 [inlined]
 [58] (::GLMakie.var"#mousebuttons#168"{Observable{Makie.MouseButtonEvent}})(window::GLFW.Window, button::GLFW.MouseButton, action::GLFW.Action, mods::Int32)
    @ GLMakie ~/.julia/packages/GLMakie/qLxLP/src/events.jl:102
 [59] _MouseButtonCallbackWrapper(window::GLFW.Window, button::GLFW.MouseButton, action::GLFW.Action, mods::Int32)
    @ GLFW ~/.julia/packages/GLFW/BWxfF/src/callback.jl:43
 [60] PollEvents
    @ ~/.julia/packages/GLFW/BWxfF/src/glfw3.jl:620 [inlined]
 [61] pollevents(screen::GLMakie.Screen{GLFW.Window})
    @ GLMakie ~/.julia/packages/GLMakie/qLxLP/src/screen.jl:449
 [62] on_demand_renderloop(screen::GLMakie.Screen{GLFW.Window})
    @ GLMakie ~/.julia/packages/GLMakie/qLxLP/src/screen.jl:937
 [63] renderloop(screen::GLMakie.Screen{GLFW.Window})
    @ GLMakie ~/.julia/packages/GLMakie/qLxLP/src/screen.jl:963
@markusgumbel markusgumbel changed the title abmplot fails when 1) number of agents changes over time and 2) custom agent_size function is used abmplot fails when 1) number of agents changes over time and 2) custom agent_size function is used May 14, 2024
@Datseris Datseris added bug Something isn't working plotting labels May 14, 2024
@Tortar
Copy link
Member

Tortar commented May 14, 2024

mmmh, I can't actually reproduce, I get

Screenshot from 2024-05-14 19-46-43

maybe GLMakie version you installed is not compatible with Agents? I have GLMakie v0.9.11 and Agents 6.0.12 installed

@Tortar
Copy link
Member

Tortar commented May 14, 2024

since you are using Agents 6.0 syntax, you have a old GLMakie version installed somehow probably

@markusgumbel
Copy link
Author

Thank you for your quick response. I also use GLMakie v0.9.11 (and Makie v0.20.10 if this is relevant).
Just to be sure: Did you play the "step model" or "run model" button? Starting the program works for me too, but not stepping through or running the model.

@Tortar
Copy link
Member

Tortar commented May 14, 2024

ah no sorry, didn't see that in your reproducer, yes the error is there also for me :-)

@Tortar
Copy link
Member

Tortar commented May 14, 2024

I investigated a bit the issue and I'm unsure of what could be the cause because this works instead:

using Agents, GLMakie

@agent struct Particle(ContinuousAgent{2,Float64}) end

function build_model()
    space = ContinuousSpace((1, 1))
    model = StandardABM(Particle, space; agent_step! = particle_step!)
    for i = 1:100
        pos = Tuple(rand(2))
        vel = Tuple(rand(2))
        add_agent!(Particle(i, pos, vel), model)
    end
    return model
end

function particle_step!(p, model)
    randomwalk!(p, model, 1.0)
    if rand() < 0.5
        remove_agent!(p, model) # Number of agents changes here.
    end
end

p_color(p::Particle) = :red

model = build_model()

figure, ax, abmobs = abmplot(model; add_controls = true, agent_color = p_color)
resize!(figure, 1200, 800)
wait(display(figure))

which is strange because internally agent_color and agent_size are treated identically. Maybe it could be an issue with Makie itself? Do you have any idea @Datseris?

@Datseris
Copy link
Member

I am trying to find where in the source code the UPDATE of plotted agents is done. I cannot, unfortunately. If you can find where the update of the scatterplot is done in the source code please paste a link here and I ll have a look.

@markusgumbel
Copy link
Author

Thanks again for your analysis. I am not sure if I can help but I looked in the sources and found in
abmplots.jl some code where the changes are "lifted" according to Makie's lift concept.

function lift_attributes(model, ac, as, am, offset)
    pos = @lift(abmplot_pos($model, $offset))
    color = @lift(abmplot_colors($model, $ac))
    marker = @lift(abmplot_markers($model, $am, $pos))
    markersize = @lift(abmplot_markersizes($model, $as))

    return pos, color, marker, markersize
end

and before

function Makie.plot!(p::_ABMPlot)
    model = p.abmobs[].model[]
...
    p.pos, p.color, p.marker, p.markersize =
        lift_attributes(p.abmobs[].model, p.agent_color, p.agent_size, p.agent_marker, p.offset)
...
    return p
end

@Datseris
Copy link
Member

Hm, I do not know the solution. I thought it was a concurrency issue with makie. Where one vector (eg size) is updated and a plot update is triggered before the other vector (e.g. color) is updated, leading to missmatch of dimensions. However, normally the plotting code works fine with agents being deleted, we have many examples and animations where this occur and agents have different colors. What is fundamentally different in this MWE that makes it not work...?

Is it really that the size is different? If we run e.g., the rock paper scissors example where each type also has different size, would we get the same error...?

@Tortar
Copy link
Member

Tortar commented May 15, 2024

yes, tried, we get the same error.

I think I found a github issue and PR which describe the culprit of this: MakieOrg/Makie.jl#3658 and JuliaGizmos/Observables.jl#109

@Datseris
Copy link
Member

great, thanks!

@Datseris Datseris added the external-dependecy problem in a dependency package label May 15, 2024
@Datseris
Copy link
Member

We can also solve this on our side by the way. Instead of using @lift, we first create the observables, and then we create an on function that does:

on(model_observable) do model
pos_observable[] = ...
size_observable[] = ...
# and at the end:
notify(pos_observable) # this is enough to trigger plot update

it is actually super sipmle to change the source code to do that.

@micmavrof
Copy link

I wanted to provide an update and also say thanks for all the great work on Agents.jl! As a newcomer to Julia, primarily interested in simulation development, I've found it to be an invaluable tool.

My goal is to visualize a simulation where both the number of agents and their visual attributes (position, size, color, and marker) change dynamically at each step.

Using the Minimum Working Example (MWE) provided earlier in this thread as a starting point, I've implemented a solution (code follows at the end) that appears to avoid the DimensionMismatch error during runtime. This was achieved by:

  1. Creating a single Observable containing all agent’s visualization data.
  2. Using @lift to derive separate observables for each property (position, size, color, marker).
  3. Setting up a model listener to update the main Observable, which should trigger updates to all derived observables simultaneously.

While this approach has successfully mitigated the DimensionMismatch error, I've encountered a new challenge: the updates to agent data don't seem to be triggering or reflecting in the visualization during the simulation run.

Given these circumstances, I would greatly appreciate more detailed guidelines about the workaround and update mechanism in Agents.jl, particularly in the context of dynamic visualizations. Specifically, a code update you could provide would be immensely helpful.

Thank you for your time and continued efforts on this excellent package.

using Agents, GLMakie, Random

@agent struct Particle(ContinuousAgent{2,Float64})
    size::Float32
    color::RGBf
    marker::Symbol
end

# Model parameters and constants
const WALK_SPEED = 0.01
const REMOVAL_CHANCE = 0.03
const ADDITION_CHANCE = 0.01
const GROWTH_RATE = 0.3
const MAX_SIZE = 30f0

mutable struct ModelParams
    n_particles::Int
end

function build_model(params::ModelParams)
    space = ContinuousSpace((1, 1))
    model = StandardABM(Particle, space; agent_step! = particle_step!, properties = params)
    for i in 1:params.n_particles
        add_particle!(model)
    end
    model
end

function add_particle!(model)
    color = RGBAf(rand(), rand(), rand(), 0.7)
    marker = rand([:circle, :rect, :star5])
    add_agent!( 
        pos = Tuple(rand(2)),  # position
        model;
        vel = Tuple(rand(2)),  # velocity
        size = 10f0,            # initial size
        color = color,
        marker = marker)
end

function particle_step!(p, model)
    randomwalk!(p, model, WALK_SPEED)
    p.size = min(p.size * (1 + GROWTH_RATE), MAX_SIZE)
    
    if rand(abmrng(model)) < REMOVAL_CHANCE
        remove_agent!(p, model)
    end
    
    if rand(abmrng(model)) < ADDITION_CHANCE
        add_particle!(model)
    end
end

function agent_data(model)
    sorted_agents = sort(collect(allagents(model)), by = a -> a.id)
    Dict(
        :pos => [Point2f(a.pos) for a in sorted_agents],
        :size => [a.size for a in sorted_agents],
        :color => [a.color for a in sorted_agents],
        :marker => [a.marker for a in sorted_agents]
    )
end

function run_simulation(; n_particles=100)
    params = ModelParams(n_particles)
    model = build_model(params)
    
    abmobs = ABMObservable(model)
    
    data_obs = Observable(agent_data(model))
    
    pos_obs = @lift($(data_obs)[:pos])
    size_obs = @lift($(data_obs)[:size])
    color_obs = @lift($(data_obs)[:color])
    marker_obs = @lift($(data_obs)[:marker])
    
    fig, ax, abmplot_output = abmplot(
        model;
        agent_pos = pos_obs,
        agent_size = size_obs,
        agent_color = color_obs,
        agent_marker = marker_obs,
        add_controls = true
    )
    
    on(abmobs.model) do m
        data_obs[] = agent_data(m)
    end
    
    resize!(fig, 1200, 800)
    display(fig)
    
    return fig, abmobs, data_obs
end

# Run the simulation
fig, abmobs, data_obs = run_simulation()

@Datseris
Copy link
Member

Thanks for your kind words!

Why don't you try what I said here: #1035 (comment) ? This needs to be done directly at the source code. I believe this will be the solution!

@micmavrof
Copy link

Thank you so much for your immediate reply!

I appreciate your suggestion, and I want to clarify that it was actually your advice that I tried to implement first. However, despite attempting various approaches, I wasn't able to completely avoid the DimensionMismatch error. As a newcomer to Julia, I found it challenging to implement the solution correctly, which is why I ended up with the version I shared in my previous comment.

Given my inexperience, I would greatly appreciate if you could provide a more detailed example of how to implement this solution correctly. Specifically, I'm unsure about:

  1. Where exactly in the code should I create the observables?
  2. How should I structure the on function to update these observables, especially considering that the number of agents can change dynamically?
  3. Are there any specific considerations for handling the varying number of agents in relation to the observables?

Any additional guidance or a small code snippet demonstrating this approach would be immensely helpful.

Thank you again for your patience and support!

@Datseris
Copy link
Member

Right, I am actually on holiday so I'll come back here at a later time with more !

@micmavrof
Copy link

While waiting for your return and reply, I've conducted a series of progressive tests to isolate the error. Here's a summary of my findings:

  1. The most enhanced Minimum Working Example (MWE) version, exhibiting all the functionality without producing the DimensionMismatch error is as follows:
using Agents
using GLMakie
using Random
using Statistics

@agent struct Particle(ContinuousAgent{2,Float64})
    size::Float64
    color::RGBAf
    marker::Symbol
    energy::Float64
    remove::Bool  # New field to mark for removal
end

function initialize_model(n_particles)
    space = ContinuousSpace((1.0, 1.0), periodic = true)
    Random.seed!(50)
    model = StandardABM(Particle, space;
        properties = Dict(
            :step => 0,
            :interaction_radius => 0.05,
            :energy_transfer => 0.1
        ),
        agent_step! = particle_step!,
        model_step! = model_step!
    )
    
    for _ in 1:n_particles
        add_agent!(
            pos = Tuple(rand(2)),  # position
            model;
            vel = (rand() - 0.5, rand() - 0.5) .* 0.01,  # Reduced velocity
            size = rand(5.0:15.0),    # size
            color = RGBAf(rand(), rand(), rand(), 0.8),
            marker = rand([:circle, :rect, :star5]),
            energy = rand(50.0:100.0),   # energy
            remove = false  # Initialize as not marked for removal
        )
    end
    
    return model
end

function particle_step!(particle, model)
    # Update position manually
    new_pos = particle.pos .+ particle.vel
    new_pos = mod.(new_pos, 1)  # Wrap around the unit square
    particle.pos = new_pos

    # Make size changes more noticeable
    particle.size = 5.0 + (particle.energy / 10.0)
    particle.size = clamp(particle.size, 5.0, 20.0)
    
    particle.energy -= 0.5  # Increased energy loss
    
    nearby_particles = nearby_agents(particle, model, model.interaction_radius)
    for nearby in nearby_particles
        if nearby.energy > particle.energy
            energy_transfer = model.energy_transfer * (nearby.energy - particle.energy)
            particle.energy += energy_transfer
            nearby.energy -= energy_transfer
        end
    end
    
    if particle.energy < 10.0 && rand() < 0.05  # Increased removal probability
        println("Marking agent $(particle.id) for removal. Current energy: $(particle.energy)")
        particle.remove = true  # Mark for removal instead of removing immediately
    end
end

function model_step!(model)
    println("Start of model_step!. Step: $(model.step), Agents: $(nagents(model))")
    
    # Remove marked agents
    for agent in allagents(model)
        if agent.remove
            println("Removing marked agent $(agent.id)")
            remove_agent!(agent, model)
        end
    end
    
    # Add new agents (as before)
    if rand() < 0.1  # Increased addition probability
        new_agent = add_agent!(
            pos = Tuple(rand(2)),  # position
            model;
            vel = (rand() - 0.5, rand() - 0.5) .* 0.01,  # Reduced velocity
            size = rand(5.0:15.0),    # size
            color = RGBAf(rand(), rand(), rand(), 0.8),
            marker = rand([:circle, :rect, :star5]),
            energy = rand(50.0:100.0),   # energy
            remove = false  # Initialize as not marked for removal
        )
        println("Added new agent $(new_agent.id)")
    end
    
    model.step += 1
    println("End of model_step!. Step: $(model.step), Agents: $(nagents(model))")
end

function run_simulation(; n_particles=100)
    model = initialize_model(n_particles)
    
    fig = Figure(size = (1000, 800))
    ax = Axis(fig[1, 1], aspect = DataAspect())
    
    xlims!(ax, 0, 1)
    ylims!(ax, 0, 1)
    
    function update_plot!(model, ax)
        empty!(ax)  # Clear the existing plot
        
        agents = collect(allagents(model))
        n = length(agents)
        
        positions = [Point2f(a.pos) for a in agents]
        sizes = [Float32(a.size) for a in agents]
        colors = [a.color for a in agents]
        markers = [a.marker for a in agents]
        
        # Create a new scatter plot
        scatter!(ax, positions, color = colors, markersize = sizes, marker = markers)
        
        println("Updated plot. Agents: $(n), Step: $(model.step)")
    end
    
    update_plot!(model, ax)
    
    button = Button(fig[2, 1], label = "Step")
    on(button.clicks) do _
        println("Step button clicked")
        Agents.step!(model, 1)
        update_plot!(model, ax)
        println("Step completed")
    end
    
    # Ensure the plot takes up most of the figure
    colsize!(fig.layout, 1, Relative(1.0))
    rowsize!(fig.layout, 1, Relative(0.9))
    
    display(fig)
    
    return fig, model
end



# Run the simulation
fig, model = run_simulation()
  1. However, if we modify the run_simulation() function as follows, the DimensionMismatch error appears:
function run_simulation(; n_particles=100)
    model = initialize_model(n_particles)
    
    fig = Figure(size = (1000, 800))
    ax = Axis(fig[1, 1], aspect = DataAspect())
    
    xlims!(ax, 0, 1)
    ylims!(ax, 0, 1)
    
    # Initial plot data
    positions = [Point2f(a.pos) for a in allagents(model)]
    sizes = [Float32(a.size) for a in allagents(model)]
    colors = [a.color for a in allagents(model)]
    markers = [a.marker for a in allagents(model)]
    
    # Create the scatter plot
    scatter_plot = scatter!(ax, positions, color = colors, markersize = sizes, marker = markers)
    
    function update_plot!(model, scatter_plot)
        try
            agents = collect(allagents(model))  # Collect agents to ensure consistent iteration
            n = length(agents)
            
            new_positions = Vector{Point2f}(undef, n)
            new_sizes = Vector{Float32}(undef, n)
            new_colors = Vector{RGBAf}(undef, n)
            new_markers = Vector{Symbol}(undef, n)
            
            for (i, a) in enumerate(agents)
                new_positions[i] = Point2f(a.pos)
                new_sizes[i] = Float32(a.size)
                new_colors[i] = a.color
                new_markers[i] = a.marker
            end
            
            # Update all plot properties at once
            scatter_plot[1] = new_positions
            scatter_plot.color = new_colors
            scatter_plot.markersize = new_sizes
            scatter_plot.marker = new_markers
            
            println("Updated plot. Agents: $(n), Step: $(model.step)")
        catch e
            println("Error in update_plot!: ", e)
            for (arr_name, arr) in zip(["positions", "sizes", "colors", "markers"], 
                                       [new_positions, new_sizes, new_colors, new_markers])
                println("$arr_name length: $(length(arr))")
            end
            rethrow(e)
        end
    end
    
    update_plot!(model, scatter_plot)
    
    button = Button(fig[2, 1], label = "Step")
    on(button.clicks) do _
        println("Step button clicked")
        Agents.step!(model, 1)
        update_plot!(model, scatter_plot)
        println("Step completed")
    end
    
    # Ensure the plot takes up most of the figure
    colsize!(fig.layout, 1, Relative(1.0))
    rowsize!(fig.layout, 1, Relative(0.9))
    
    display(fig)
    
    return fig, model, scatter_plot
end
  1. Additionally, I've discovered what appears to be a bug in src\spaces\continuous.jl`remove_agent_from_space!()`. After approximately 140 steps, the following error appears:

...
Step button clicked
Start of model_step!. Step: 136, Agents: 120
End of model_step!. Step: 137, Agents: 120
Updated plot. Agents: 120, Step: 137
Step completed
Step button clicked
Marking agent 38 for removal. Current energy: 9.515653346961837
Start of model_step!. Step: 137, Agents: 120
Removing marked agent 38
Error in callback:
MethodError: no method matching iterate(::Nothing)

Closest candidates are:
iterate(::LibGit2.GitConfigIter)
@ LibGit2 C:\Users.julia\juliaup\julia-1.10.4+0.x64.w64.mingw32\share\julia\stdlib\v1.10\LibGit2\src\config.jl:225
iterate(::LibGit2.GitConfigIter, ::Any)
@ LibGit2 C:\Users.julia\juliaup\julia-1.10.4+0.x64.w64.mingw32\share\julia\stdlib\v1.10\LibGit2\src\config.jl:225
iterate(::Cmd)
@ Base process.jl:678
...

Stacktrace:
[1] _deleteat!(a::Vector{Int64}, inds::Nothing, dltd::Base.Nowhere)
@ Base .\array.jl:1658
[2] _deleteat!(a::Vector{Int64}, inds::Nothing)
@ Base .\array.jl:1657
[3] deleteat!(a::Vector{Int64}, inds::Nothing)
@ Base .\array.jl:1633
[4] remove_agent_from_space!(a::Particle, model::StandardABM{ContinuousSpace{2, true, Float64, typeof(Agents.no_vel_update)}, Particle, Dict{Int64, Particle}, Tuple{DataType}, typeof(particle_step!), typeof(model_step!), typeof(Agents.Schedulers.fastest), Dict{Symbol, Real}, TaskLocalRNG}, cell_index::Tuple{Int64, Int64})
@ Agents C:\Users.julia\packages\Agents\XdvNR\src\spaces\continuous.jl:142
[5] remove_agent_from_space!
@ C:\Users.julia\packages\Agents\XdvNR\src\spaces\continuous.jl:140 [inlined]
[6] remove_agent!
@ C:\Users.julia\packages\Agents\XdvNR\src\core\space_interaction_API.jl:145 [inlined]
[7] model_step!(model::StandardABM{ContinuousSpace{2, true, Float64, typeof(Agents.no_vel_update)}, Particle, Dict{Int64, Particle}, Tuple{DataType}, typeof(particle_step!), typeof(model_step!), typeof(Agents.Schedulers.fastest), Dict{Symbol, Real}, TaskLocalRNG})
@ Main c:\Users\source\repos\test01_jl\MWE_04d.jl:81

  1. When handling this exception, the resulting stack trace is:

...
Removing marked agent 75
Error during step or plot update:
MethodError(iterate, (nothing,), 0x0000000000007c19)
Base.StackTraces.StackFrame[_deleteat!(a::Vector{Int64}, inds::Nothing, dltd::Base.Nowhere) at array.jl:1658, _deleteat!(a::Vector{Int64}, inds::Nothing) at array.jl:1657, deleteat!(a::Vector{Int64}, inds::Nothing) at array.jl:1633, remove_agent_from_space!(a::Particle, model::StandardABM{Contin(event::MouseEvent) at mousestatemachine.jl:94, #invokelatest#2 at essentials.jl:892 [inlined], invokelatest at essentials.jl:889 [inlined], notify at Observables.jl:206 [inlined], setindex! at Observables.jl:123 [inlined], (::Makie.var"#1537#1539"{Scene, Base.RefValue{Bool}, Base.RefValue{Union{Nothing, Makie.Mouse.Button}}, Base.RefValue{Float64}, Base.RefValue{Float64}, Base.RefValue{Bool}, Base.RefValue{Bool}, Base.RefValue{Union{Nothing, Makie.Mouse.Button}}, Base.RefValue{Bool}, Base.RefValue{Point{2, Float32}}, Base.RefValue{Point{2, Float64}}, Base.RefValue{Makie.Mouse.Action}, Observable{MouseEvent}, Float64, Module})(event::Makie.MouseButtonEvent) at mousestatemachine.jl:298, #invokelatest#2 at essentials.jl:892 [inlined], invokelatest at essentials.jl:889 [inlined], notify at Observables.jl:206 [inlined], setindex! at Observables.jl:123 [inlined], (::GLMakie.var"#mousebuttons#167"{Observable{Makie.MouseButtonEvent}})(window::GLFW.Window, button::GLFW.MouseButton, action::GLFW.Action, mods::Int32) at events.jl:104, _MouseButtonCallbackWrapper(window::GLFW.Window, button::GLFW.MouseButton, action::GLFW.Action, mods::Int32) at callback.jl:43, PollEvents at glfw3.jl:702 [inlined], pollevents(screen::GLMakie.Screen{GLFW.Window}, frame_state::Makie.TickState) at screen.jl:483, on_demand_renderloop(screen::GLMakie.Screen{GLFW.Window}) at screen.jl:979, renderloop(screen::GLMakie.Screen{GLFW.Window}) at screen.jl:1007, (::GLMakie.var"#68#69"{GLMakie.Screen{GLFW.Window}})() at screen.jl:867]

These findings suggest that the DimensionMismatch error is related to how the scatter plot is updated when the number of agents changes. The error in remove_agent_from_space!() might be a separate issue, but it could be related to how agents are managed in the continuous space.

I hope this information helps in diagnosing and resolving these issues. Moreover, the MWE code can be used as a testbed for further experimentation.
Please let me know if you need any further details or if you'd like me to conduct any additional tests.

@Tortar
Copy link
Member

Tortar commented Aug 30, 2024

I don't have much time at the moment to further analyze the situation but I think this issue is pretty similar: MakieOrg/Makie.jl#2441, I think that you can understand from here what can be changed in Agents, thanks for the MWEs!

@micmavrof
Copy link

Thank you for your reply and for taking the time to respond. I appreciate the link to the Makie.jl issue #2441, which does indeed seem related.
I want to clarify that I have already explored various code variants based on your earlier suggestions in this thread, including approaches similar to those discussed in the Makie.jl issue you mentioned. Unfortunately, I haven't been able to resolve the DimensionMismatch error completely in the context of Agents.jl.

Given your expertise with Agents.jl, your insights on how to best implement the solution proposed in the Makie.jl issue within the Agents.jl framework would be extremely valuable. Specifically, I'm unsure how to properly synchronize the updates of multiple observables in the context of an agent-based model where the number of agents can change dynamically.

Furthermore, during these tests and as I mentioned above (please see my previous message Agents.jl/issues/1035#issuecomment-2321529956, points 3 and 4) I think I have come across another issue with Agents.jl, specifically in the remove_agent_from_space! function. Could you please check that, too?

@Tortar
Copy link
Member

Tortar commented Sep 6, 2024

I have just taken a look at remove_agent_from_space!, I don't see anything buggy but the error message could be nonetheless be improved, see #1073 (which also removes a dynamic dispatch though it is innocuous), so the error it's probably still related to the concurrency issue in plots updates

@micmavrof
Copy link

Thank you for your reply as well as for your actions (#1073). I appreciate your attention to this matter.
I suppose that this presents an excellent opportunity to thoroughly examine the resilience of Agents.jl code under dynamic agent addition and removal scenarios; this is indeed a crucial feature for many simulations, and ensuring its robustness is vital for the package's reliability.

I'd like to provide an update on the current issue: I've developed a version of run_simulation() that successfully runs without producing the DimensionMismatch error. This holds true whether using scatter!() or abmplot!() functions. Here's the code:

function run_simulation(; n_particles=100)
    model = initialize_model(n_particles)
    model_obs = Observable(model)
    
    fig = Figure(size = (1000, 800))
    ax = Axis(fig[1, 1], aspect = DataAspect())
    
    xlims!(ax, 0, 1)
    ylims!(ax, 0, 1)
    
    # Create observables for plot data
    pos_obs = Observable(Point2f[])
    size_obs = Observable(Float32[])
    color_obs = Observable(RGBAf[])
    marker_obs = Observable(Symbol[])

    function update_observables(ax, model)
        empty!(ax)  # Clear the existing plot
        agents = collect(allagents(model))
        pos_obs.val = [Point2f(a.pos) for a in agents]
        size_obs.val = [Float32(a.size) for a in agents]
        color_obs.val = [a.color for a in agents]
        marker_obs[] = [a.marker for a in agents]  # This will trigger the update
    end

    # Initial update
    update_observables(ax, model)

    # Create the scatter plot with observables
    # Either of the following commands run without error
    # @lift(scatter!(ax, $pos_obs, color = $color_obs, markersize = $size_obs, marker = $marker_obs))
    @lift(abmplot!(ax, model; positions = $pos_obs, agent_color = $color_obs, agent_size = $size_obs, agent_marker = $marker_obs))

    on(model_obs) do m
        update_observables(ax, m)
    end

    button = Button(fig[2, 1], label = "Step")
    on(button.clicks) do _
        println("Step button clicked")
        Agents.step!(model_obs[], 1)
        model_obs[] = model_obs[]  # Trigger observable update
        println("Step completed")
    end
    
    # Ensure the plot takes up most of the figure
    colsize!(fig.layout, 1, Relative(1.0))
    rowsize!(fig.layout, 1, Relative(0.9))
    
    display(fig)
    
    return fig
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working external-dependecy problem in a dependency package plotting
Projects
None yet
Development

No branches or pull requests

4 participants