JuliaDynamics / Agents.jl

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

A compactify macro to help to create a unique-type agent for multi-agent models #947

Closed Tortar closed 6 months ago

Tortar commented 7 months ago

I though quite a bit on how to compactify agents when you want only one agent type to avoid abstract containers and dynamic dispatch, and I finally found a satisfying solution in my opinion.

This is the proposed syntax for the macro:

@compact struct Animal(GridAgent{2})
    @agent struct Wolf
        energy::Float64 = 0.5
    ground_speed::Float64
    const fur_color::Symbol
    end
    @agent struct Hawk
    energy::Float64 = 0.1
    ground_speed::Float64
    flight_speed::Float64
    end
end

this is the code it would generate:

using Agents, LazilyInitializedFields # to be able to run the below code

@lazy mutable struct Animal <: AbstractAgent
    const id::Int
    pos::NTuple{2,Int}
    type::Symbol
    energy::Float64
    ground_speed::Float64
    @lazy flight_speed::Float64
    @lazy fur_color::Symbol
end

function Wolf(; id, pos, ground_speed, fur_color, energy = 0.5)
    return Animal(id, pos, :wolf, energy, ground_speed, uninit, fur_color)
end

function Wolf(id, pos, energy, ground_speed, fur_color)
    return Animal(id, pos, :wolf, energy, ground_speed, uninit, fur_color)
end

function Hawk(; id, pos, ground_speed, flight_speed, energy = 0.1)
    return Animal(id, pos, :hawk, energy, ground_speed, flight_speed, uninit)
end

function Hawk(id, pos, energy, ground_speed, flight_speed)
    return Animal(id, pos, :hawk, energy, ground_speed, flight_speed, uninit)
end

Generating this syntax from the initial code is a bit hard but it is clearly possible, indeed the code above already works e.g.

julia> Hawk(1, (1,1), 1.0, 1.0, 1.0)
Animal(1, (1, 1), :hawk, 1.0, 1.0, 1.0, uninit)

julia> Wolf(1, (1,1), 1.0, 1.0, :black)
Animal(1, (1, 1), :wolf, 1.0, 1.0, uninit, :black)

@lazy comes from LazilyInitializedFields.jl which in my experiments (and their benchmarks) doesn't incur almost any overhead.

Obviously this involves the memory trade-off of having more fields than necessary, so this is just an improved alternative for multi type models, not a substitution.

Notice though some cool features of this implementation:

cc @Datseris because I know you thought a lot about this too, so I want that you see this concrete proposal :-)

Datseris commented 6 months ago

This is great. This is truly great. The only thing we need to make sure is that add_agent! plays well with it and works as expected, so that

add_agent!(Wolf, only_wolf_fields...)

does what it should and initializes an Animal with correct id, pos, and type.

SHould we add this option for v6, or you want to do it later? The benefit of doing it at v6 would be that this rather important perforamnce feature would be included in the v6 announcement post.

Datseris commented 6 months ago

You are right, this doesn't totally resolve the multi-agent "problem". But it gives a very important option of having only one agent type. I think in the majority of use cases this option would have performance gains.

Tortar commented 6 months ago

It would be cool to have this in v6 I agree, but it needs some more work, so I don't promise to finish it up in the very near future :D

mastrof commented 6 months ago

This looks great. Haven't tried to clone the branch for testing, but if Wolf and Hawk are not defined as types, does this mean that instead of dispatching as f(::Wolf) and f(::Hawk) one would always need to check against Animal.type following a structure of the form f(a::Animal) = a.type == :hawk ? f_hawk(a) : f_wolf(a)?

I wonder, if it would make sense to dispatch on the value of type? E.g. f(::Val{:hawk}), f(::Val{:wolf})... I'm a bit clueless here, would this have negative performance impacts?

Tortar commented 6 months ago

I know you like macros @mastrof :D

I think that it is an interesting idea, it will costs more because this is dynamic dispatch vs branching and branching wins, but in reality the real perf benefit here is to avoid abstract containers, so it will not change much adding that for only agent_step!, I will try it out on the benchmark I re-setuped for testing the macro on the PR.

Tortar commented 6 months ago

tried:

Seems fine to me

branching:

561.657 ms (23076750 allocations: 1001.82 MiB)

with Val

589.242 ms (23076750 allocations: 1001.82 MiB)

(Referring to branching_faster_than_dispatch.jl file)

Mod Version:

function agent_step!(agent::GridAgentAll, model2)
    agent_step!(agent, model2, Val(agent.type))
end
agent_step!(agent, model2, ::Val{:gridagentone}) = randomwalk!(agent, model2)
function agent_step!(agent, model2, ::Val{:gridagenttwo})
    agent.one += rand(abmrng(model2))
    agent.two = rand(abmrng(model2), Bool)
end
function agent_step!(agent, model2, ::Val{:gridagentthree})
    if any(a-> a.type == :gridagenttwo, nearby_agents(agent, model2))
        agent.two = true
        randomwalk!(agent, model2)
    end
end
function agent_step!(agent, model2, ::Val{:gridagentfour})
    agent.one += sum(a.one for a in nearby_agents(agent, model2))
end
function agent_step!(agent, model2, ::Val{:gridagentfive})
    targets = filter!(a->a.one > 1.0, collect(nearby_agents(agent, model2, 3)))
    if !isempty(targets)
        idx = argmax(map(t->euclidean_distance(agent, t, model2), targets))
        farthest = targets[idx]
        walk!(agent, sign.(farthest.pos .- agent.pos), model2)
    end
end
function agent_step!(agent, model2, ::Val{:gridagentsix})
    agent.eight += sum(rand(abmrng(model2), (0, 1)) for a in nearby_agents(agent, model2))
end
Tortar commented 6 months ago

But if you do something like this inside the step itself I expect it will have a noticeable impact on performance

Tortar commented 6 months ago

Actually I created a package which does more than what it is described here and it could allow memory efficiency to approach the one of multiple types in many cases! It is at https://github.com/Tortar/MixedStructTypes.jl if you want to take a look