JuliaDynamics / Agents.jl

Agent-based modeling framework in Julia
https://juliadynamics.github.io/Agents.jl/stable/
MIT License
763 stars 125 forks source link

Error when collecting count of agents where a model initialises with none of a certain type. #446

Open jacobusmmsmit opened 3 years ago

jacobusmmsmit commented 3 years ago

Describe the bug Initialising a model with 0 agent of a certain type, then passing adata to the run! function where you count the number of each type gives this error: ERROR: ArgumentError: collection must be non-empty

Minimal Working Example In the predator-prey example, you can set n_wolves = 0 and the error will appear when run.

model = initialize_model(
    n_wolves = 0,
    dims = (25, 25),
    Δenergy_sheep = 5,
    sheep_reproduce = 0.2,
    wolf_reproduce = 0.08,
)
results, _ = run!(model, agent_step!, n_steps; adata)

Agents.jl version

Agents v4.1.3 (note: this was not an issue in Agents v3.7 as this is what I was previously using before upgrading and having this break.) Julia v1.5.3

Libbum commented 3 years ago

What is adata for this case? Could be a bug, but perhaps this was a conscious change we made for 4.x, it'll depend on your adata.

jacobusmmsmit commented 3 years ago

adata is defined as in the example code:

sheep(a) = typeof(a) == Sheep
wolves(a) = typeof(a) == Wolf
grass(a) = typeof(a) == Grass && a.fully_grown
adata = [(sheep, count), (wolves, count), (grass, count)]
Libbum commented 3 years ago

OK, thanks—I'll take a look.

Libbum commented 3 years ago

this line is the problem. It assumes at least one agent of each type in the model. Should be a quick fix.

Libbum commented 3 years ago

I quickly identified the error just before bed last night. Looking at it a little more closely this morning, this may be a more nuanced problem than originally anticipated.

The behavior we should expect here is something more like the single case:

mutable struct Agent3 <: AbstractAgent
    id::Int
    pos::Tuple{Int,Int}
    weight::Float64
end

model = ABM(Agent3, GridSpace((5,5)))
agent3(a) = a isa Agent3
adata = [(agent3, count)]

julia> data, _ = run!(model, dummystep, 1; adata)
ERROR: ArgumentError: Model must have at least one agent to initialize data collection

So the current argument error is not helpful and needs to be fixed at the very least. If the model has no instance of an agent type it has no way of identifying the properties of that agent in the general case.

In v3.7, multiagent models like predator prey were not properly supported by the collection routines, whereas they are now. This allowed for an edge case that you're using in 3.7, where the model has one or more agent, but not one or more agent of each type.

I'd need to know a little more about your motivations in this example to be able to provide functionality for it. This line of questioning goes for single-agent models too for the most part.

Datseris commented 3 years ago

I don't agree that this is a bug, or something to be fixed. As Julia says, collection must be non-empty, You need at least one agent to collect agent data, that's normal to expect.

We can change the error to say Model must have at least one agent to initialize data collection instead if adata is not nothing and nagents(model) == 0, but that's about it.

Datseris commented 3 years ago

Tim's last idea (using the data collection interface) for a specific case where a model must start without any agents is the best solution. The convenience of run! cannot be applied in this case.

jacobusmmsmit commented 3 years ago

Thank you for the responses. In my case there will be agents of all type in my simulation, just perhaps not at the beginning. I am trying to construct a quiver plot of the one step population makeup over a triangle simplex of possible population makeups (I have three types in my model, hence triangle simplex).

image

From the image you can see that this needs to start sometimes where there are indeed no agents of a certain type. I was thinking of doing this with a paramscan but I'm not sure how best to do so.

jacobusmmsmit commented 3 years ago

For reference here is how I am currently doing this in Agents.jl v3.7

sitters(a) = typeof(a) == Sitter
identifiers(a) = typeof(a) == Identifier
cheaters(a) = typeof(a) == Cheater
adata = [(sitters, count), (identifiers, count), (cheaters, count)]

total_agents = 30
for i in 0:total_agents, j in 0:total_total_agents - i
    initialise_agents!(model, i, j, total_agents - i - j)

    results, _ = run!(model, agent_step!, model_step!, 1; adata=adata, replicates=100);

    results = results |>
        df -> filter(:step => !=(0), df) |>
        df -> combine(df, :count_sitters => mean => :xend, :count_identifiers => mean => :yend)
    results[:, :xstart] .= i
    results[:, :ystart] .= j
    final_results = [final_results; results]
end

It is rather rudimentary.

Libbum commented 3 years ago

Since we cannot know how your (or anyone else's) agents look before hand, we cannot provide this service in general via run!, so you'll need to swap that out with a custom run function and use the data collection interface.

Using the current implementation as a basis, something like:

model = init_function(...) # Add at least one agent of each type
results = init_agent_dataframe(model, adata) # This gets us the initial table layout correct
total_agents = 30
for i in 0:total_agents, j in 0:total_total_agents - i
    # My understanding is that this function calls `genocide!`, so initial agents will be cleared
    initialise_agents!(model, i, j, total_agents - i - j)

    # `results` will be updated
    custom_run!(results, model, agent_step!, model_step!, 1; adata)
    ...
end

function custom_run!(df_agent, model, agent_step!, model_step!, n; adata=nothing)
    s = 0
    while s < n
        collect_agent_data!(df_agent, model, adata, s)
        step!(model, agent_step!, model_step!, 1)
        s += 1
    end
    collect_agent_data!(df_agent, model, adata, s)
    return nothing
end

Replicates are handled this way. How this is extended nicely for cases like this is in the plans for the future. We've been working recently on seeding #420 to get replicates to work better #415. For the moment though, a custom implementation that wraps your custom_run! is the best bet to get this working in v4.x.

Libbum commented 3 years ago

@jacobusmmsmit I'm coming back to this issue soon since we just sorted out some big questions about seeding and replicates. I think we can now do what you're asking a bit easier.

mingR commented 2 years ago

this line is the problem. It assumes at least one agent of each type in the model. Should be a quick fix.

The error message has been confusing, finally found the answer here, thanks!