From d3c4d47b858f278a17243856624ea23bd7188c61 Mon Sep 17 00:00:00 2001 From: Tortar Date: Sun, 7 Jul 2024 03:48:01 +0200 Subject: [PATCH 01/54] Update to DynamicSumTypes 3: remove @multiagent --- Project.toml | 2 +- docs/src/api.md | 5 - examples/event_rock_paper_scissors.jl | 38 +---- src/Agents.jl | 1 - src/core/agents.jl | 227 +------------------------- src/core/model_event_queue.jl | 80 ++++----- src/deprecations.jl | 8 + src/simulations/sample.jl | 6 +- src/submodules/schedulers.jl | 59 ------- 9 files changed, 53 insertions(+), 373 deletions(-) diff --git a/Project.toml b/Project.toml index d0e8a8a0ae..8c7d09e225 100644 --- a/Project.toml +++ b/Project.toml @@ -47,7 +47,7 @@ DataFrames = "0.21, 0.22, 1" DataStructures = "0.18" Distributed = "1" Distributions = "0.25" -DynamicSumTypes = "2" +DynamicSumTypes = "3" Downloads = "1" GraphMakie = "0.5" Graphs = "1.4" diff --git a/docs/src/api.md b/docs/src/api.md index af4811c96b..97846797a1 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -31,11 +31,6 @@ AgentEvent ```@docs @agent AbstractAgent -@multiagent -kindof -allkinds -@dispatch -@finalize_dispatch ``` ### Minimal agent types diff --git a/examples/event_rock_paper_scissors.jl b/examples/event_rock_paper_scissors.jl index 423a703662..8859ad26a0 100644 --- a/examples/event_rock_paper_scissors.jl +++ b/examples/event_rock_paper_scissors.jl @@ -37,14 +37,11 @@ using Agents -# and defining the three agent types using [`multiagent`](@ref) -# (see the main [Tutorial](@ref) if you are unfamiliar with [`@multiagent`](@ref)). +# and defining the three agent types -@multiagent struct RPS(GridAgent{2}) - @subagent struct Rock end - @subagent struct Paper end - @subagent struct Scissors end -end +@agent struct Rock(GridAgent{2}) end +@agent struct Paper(GridAgent{2}) end +@agent struct Scissors(GridAgent{2}) end # %% #src @@ -67,27 +64,10 @@ function attack!(agent, model) return end -# for the attack!(agent, contender) function we could either use some -# branches based on the values of `kindof` - -function attack!(agent::RPS, contender::RPS, model) - kind = kindof(agent) - kindc = kindof(contender) - if kind === :Rock && kindc === :Scissors - remove_agent!(contender, model) - elseif kind === :Scissors && kindc === :Paper - remove_agent!(contender, model) - elseif kind === :Paper && kindc === :Rock - remove_agent!(contender, model) - end -end - -# or use the @dispatch macro for convenience - -@dispatch attack!(::RPS, ::RPS, model) = nothing -@dispatch attack!(::Rock, contender::Scissors, model) = remove_agent!(contender, model) -@dispatch attack!(::Scissors, contender::Paper, model) = remove_agent!(contender, model) -@dispatch attack!(::Paper, contender::Rock, model) = remove_agent!(contender, model) +attack!(::AbstractAgent, ::AbstractAgent, model) = nothing +attack!(::Rock, contender::Scissors, model) = remove_agent!(contender, model) +attack!(::Scissors, contender::Paper, model) = remove_agent!(contender, model) +attack!(::Paper, contender::Rock, model) = remove_agent!(contender, model) # The movement function is equally simple due to # the many functions offered by Agents.jl [API](@ref). @@ -158,7 +138,7 @@ attack_event = AgentEvent(action! = attack!, propensity = attack_propensity) reproduction_event = AgentEvent(action! = reproduce!, propensity = reproduction_propensity) # The movement event does not apply to rocks however, -# so we need to specify the agent "kinds" that it applies to, +# so we need to specify the agent types that it applies to, # which is `(:Scissors, :Paper)`. # Additionally, we would like to change how the timing of the movement events works. # We want to change it from an exponential distribution sample to something else. diff --git a/src/Agents.jl b/src/Agents.jl index f53145e536..8c5857c813 100644 --- a/src/Agents.jl +++ b/src/Agents.jl @@ -14,7 +14,6 @@ using Graphs using DataFrames using MacroTools using DynamicSumTypes -export DynamicSumTypes import ProgressMeter using Random using StaticArrays: SVector diff --git a/src/core/agents.jl b/src/core/agents.jl index bed0a3a1b6..47dfbe95fb 100644 --- a/src/core/agents.jl +++ b/src/core/agents.jl @@ -1,5 +1,4 @@ -export AbstractAgent, @agent, @multiagent, @dispatch, @finalize_dispatch, NoSpaceAgent, kindof, allkinds -using DynamicSumTypes: allkinds +export AbstractAgent, @agent, NoSpaceAgent ########################################################################################### # @agent @@ -228,165 +227,6 @@ function _agent(struct_repr) return expr end -########################################################################################### -# @multiagent -########################################################################################### -""" - @multiagent struct YourAgentType{X,Y}(AgentTypeToInherit) [<: OptionalSupertype] - @subagent FirstAgentSubType{X} - first_property::X # shared with second agent - second_property_with_default::Bool = true - end - @subagent SecondAgentSubType{X,Y} - first_property::X = 3 - third_property::Y - end - # etc... - end - -Define multiple agent "subtypes", which are actually only variants of a unique -overarching type `YourAgentType`. This means that all "subtypes" are conceptual: they are simply -convenience functions defined that initialize the common proper type correctly -(see examples below for more). Because the "subtypes" are not real Julia `Types`, -you cannot use multiple dispatch on them. You also cannot distinguish them -on the basis of `typeof`, but need to use instead the [`kindof`](@ref) function. -That is why these "types" are often referred to as "kinds" in the documentation. -See also the [`allkinds`](@ref) function for a convenient way to obtain all kinds. - -See the [Tutorial](@ref) or the [performance comparison versus `Union` types](@ref multi_vs_union) -for why in most cases it is better to use `@multiagent` than making multiple -agent types manually. See [`@dispatch`](@ref) (also highlighted in the [Tutorial](@ref)) -for a multiple-dispatch-like syntax to use with `@multiagent`. - -Two different versions of `@multiagent` can be used by passing either `:opt_speed` or -`:opt_memory` as the first argument (before the `struct` keyword). -The first optimizes the agents representation for -speed, the second does the same for memory, at the cost of a moderate drop in performance. -By default it uses `:opt_speed`. - -## Examples - -Let's say you have this definition: - -``` -@multiagent :opt_speed struct Animal{T}(GridAgent{2}) - @subagent struct Wolf - energy::Float64 = 0.5 - ground_speed::Float64 - const fur_color::Symbol - end - @subagent struct Hawk{T} - energy::Float64 = 0.1 - ground_speed::Float64 - flight_speed::T - end -end -``` - -Then you can create `Wolf` and `Hawk` agents normally, like so - -``` -hawk_1 = Hawk(1, (1, 1), 1.0, 2.0, 3) -hawk_2 = Hawk(; id = 2, pos = (1, 2), ground_speed = 2.3, flight_speed = 2) -wolf_1 = Wolf(3, (2, 2), 2.0, 3.0, :black) -wolf_2 = Wolf(; id = 4, pos = (2, 1), ground_speed = 2.0, fur_color = :white) -``` - -It is important to notice, though, that the `Wolf` and `Hawk` types are just -conceptual and all agents are actually of type `Animal` in this case. -The way to retrieve the variant of the agent is through the function `kindof` e.g. - -``` -kindof(hawk_1) # :Hawk -kindof(wolf_2) # :Wolf -``` - -See the [rabbit_fox_hawk](@ref) example to see how to use this macro in a model. - -## Current limitations - -- Impossibility to inherit from a compactified agent. -""" -macro multiagent(version, struct_repr) - expr = _multiagent(version, struct_repr) - return esc(expr) -end - -macro multiagent(struct_repr) - expr = _multiagent(QuoteNode(:opt_speed), struct_repr) - return esc(expr) -end - -function _multiagent(version, struct_repr) - new_type, base_type_spec, abstract_type, agent_specs = decompose_struct_base(struct_repr) - base_fields = compute_base_fields(base_type_spec) - agent_specs_with_base = [] - for a_spec in agent_specs - @capture(a_spec, @subagent astruct_spec_) - int_type, new_fields = decompose_struct(astruct_spec) - push!(agent_specs_with_base, - :(@kwdef mutable struct $int_type - $(base_fields...) - $(new_fields...) - end)) - end - t = :($new_type <: $abstract_type) - c = @capture(new_type, new_type_n_{new_params__}) - if c == false - new_type_n = new_type - new_params = [] - end - new_params_no_constr = [p isa Expr && p.head == :(<:) ? p.args[1] : p for p in new_params] - new_type_no_constr = :($new_type_n{$(new_params_no_constr...)}) - a_specs = :(begin $(agent_specs_with_base...) end) - if version == QuoteNode(:opt_speed) - expr = quote - DynamicSumTypes.@sum_structs :on_fields $t $a_specs - Agents.ismultiagentcompacttype(::Type{$(namify(new_type))}) = true - DynamicSumTypes.export_variants($(namify(t))) - end - elseif version == QuoteNode(:opt_memory) - expr = quote - DynamicSumTypes.@sum_structs :on_types $t $a_specs - Agents.ismultiagentsumtype(::Type{$(namify(new_type))}) = true - DynamicSumTypes.export_variants($(namify(t))) - end - else - error("The version of @multiagent chosen was not recognized, use either `:opt_speed` or `:opt_memory` instead.") - end - - expr_multiagent = :(Agents.ismultiagenttype(::Type{$(namify(new_type))}) = true) - if new_params != [] - if version == QuoteNode(:opt_speed) - expr_multiagent_p = quote - Agents.ismultiagenttype(::Type{$(new_type_no_constr)}) where {$(new_params...)} = true - Agents.ismultiagentcompacttype(::Type{$(new_type_no_constr)}) where {$(new_params...)} = true - end - else - expr_multiagent_p = quote - Agents.ismultiagenttype(::Type{$(new_type_no_constr)}) where {$(new_params...)} = true - Agents.ismultiagentsumtype(::Type{$(new_type_no_constr)}) where {$(new_params...)} = true - end - end - else - expr_multiagent_p = :() - end - - expr = quote - $expr - $expr_multiagent - $expr_multiagent_p - nothing - end - - return expr -end - -# This function is extended in the `@multiagent` macro -ismultiagenttype(::Type) = false -ismultiagentsumtype(::Type) = false -ismultiagentcompacttype(::Type) = false - function decompose_struct_base(struct_repr) if !@capture(struct_repr, struct new_type_(base_type_spec_) <: abstract_type_ new_fields__ end) @capture(struct_repr, struct new_type_(base_type_spec_) new_fields__ end) @@ -413,68 +253,3 @@ function compute_base_fields(base_type_spec) @capture(base_agent, mutable struct _ <: _ base_fields__ end) return base_fields end - -""" - kindof(agent::AbstractAgent) → kind::Symbol - -Return the "kind" (instead of type) of the agent, which is the name given to the -agent subtype when it was created with [`@multiagent`](@ref). -""" -function DynamicSumTypes.kindof(a::AbstractAgent) - throw(ArgumentError("Agent of type $(typeof(a)) has not been created via `@multiagent`.")) -end - -""" - allkinds(AgentType::Type) → kinds::Tuple - -Return all "kinds" that compose the given agent type that was generated via -the [`@multiagent`](@ref) macro. The kinds are returned as a tuple of `Symbol`s. -""" -function DynamicSumTypes.allkinds(a::Type{<:AbstractAgent}) - (nameof(a), ) # this function is extended automatically in the macro -end - -""" - @dispatch f(args...) - -A macro to enable multiple-dispatch-like behavior for the function `f`, -for various agent kinds generated via the [`@multiagent`](@ref) macro. -For an illustration of its usage see the [Tutorial](@ref). - -If you are creating your own module/package that uses Agents.jl, and you -are using `@dispatch` inside it, then you need to put [`@finalize_dispatch`](@ref)`()` -before the module end (but after all `@dispatch` calls). -""" -macro dispatch(f_def) - return esc(:(DynamicSumTypes.@pattern $f_def)) -end - -""" - @finalize_dispatch - -A macro to finalize the definitions of the methods generated by the -`@dispatch` macro when used in a module. It just needs to be used -once at the end of the module if no `@dispatch` method is used inside -of it. Otherwise use it also before the invocations of the methods. - -## Examples - -```julia -module SomeModel - -@multiagent struct MultiAgent(NoSpaceAgent) - @subagent SubAgent1 end - @subagent SubAgent2 end -end - -@dispatch MethodSub1(::SubAgent1) = 1 -@dispatch MethodSub1(::SubAgent2) = 1 - -@finalize_dispatch() - -end -``` -""" -macro finalize_dispatch() - return esc(:(DynamicSumTypes.@finalize_patterns)) -end diff --git a/src/core/model_event_queue.jl b/src/core/model_event_queue.jl index dd4d929621..73e0fa0f4e 100644 --- a/src/core/model_event_queue.jl +++ b/src/core/model_event_queue.jl @@ -14,13 +14,7 @@ An event instance that can be given to [`EventQeueABM`](@ref). - `propensity = 1.0`: it can be either a constant real number, or a function `propensity(agent, model)` that returns the propensity of the event. This function is called when a new event is generated for the given `agent`. -- `kinds = nothing`: the kinds of agents the `action!` function can be applied to. - As [`EventQueueABM`](@ref) only works with [`@multiagent`](@ref), the - agent kinds are `Symbol`s. The default value `nothing` means that the `action!` - may apply to any kind of agents. Otherwise, it must a be **tuple** of `Symbol`s - representing the agent kinds, such as `(:Rock, :Paper, :Scissors)`. - A tuple must still be used if the action applies to only one kind of agent, - such as `(:Rock, )` (notice the closing comma). +- `types = AbstractAgent`: the types of agents the `action!` function can be applied to. - `timing = Agents.exp_propensity`: decides how long after its generation the event should trigger. By default the time is a randomly sampled time from an exponential distribution with parameter the total propensity of all applicable events to the agent. @@ -31,10 +25,10 @@ An event instance that can be given to [`EventQeueABM`](@ref). Notice that when using the [`add_event!`](@ref) function, `propensity, timing` are ignored if `event_idx` and `t` are given. """ -@kwdef struct AgentEvent{F<:Function, P, A, T<:Function} - action!::F +Base.@kwdef struct AgentEvent{F<:Function, P, A<:Type, T<:Function} + action!::F = dummystep propensity::P = 1.0 - kinds::A = nothing + types::A = AbstractAgent timing::T = exp_propensity end @@ -44,7 +38,7 @@ struct EventQueueABM{ S<:SpaceType, A<:AbstractAgent, C<:ContainerType{A}, - P,E,R<:AbstractRNG,ET,PT,FPT,Q} <: AgentBasedModel{S} + P,E,R<:AbstractRNG,ET,PT,FPT,TI,Q} <: AgentBasedModel{S} # core ABM stuff agents::C space::S @@ -54,9 +48,9 @@ struct EventQueueABM{ time::Base.RefValue{Float64} # Specific to event queue events::E - kind_to_index::Dict{Symbol, Int} - idx_events_each_kind::ET - propensities_each_kind::PT + type_to_idx::TI + idx_events_each_type::ET + propensities_each_type::PT idx_func_propensities_each_type::FPT event_queue::Q autogenerate_on_add::Bool @@ -91,7 +85,7 @@ The events have four pieces of information: first all applicable events for that agent are collected. Then, their propensities are calculated. The event generated then is selected randomly by weighting each possible event by its propensity. -3. The agent kinds(s) the event applies to. By default it applies to all kinds. +3. The agent type(s) the event applies to. By default it applies to all types. 4. The timing of the event, i.e., when should it be triggered once it is generated. By default this is an exponentially distributed random variable divided by the propensity of the event. I.e., it follows a Poisson process with the propensity @@ -169,28 +163,26 @@ function EventQueueABM( # the queue stores pairs of (agent ID, event index) mapping them to their trigger time queue = BinaryHeap(Base.By(last), Pair{Tuple{I, Int}, Float64}[]) - agent_kinds = allkinds(A) - kind_to_index = Dict(kind => i for (i, kind) in enumerate(agent_kinds)) + agent_types = union_types(A) + type_to_idx = Dict(t => i for (i, t) in enumerate(agent_types)) # precompute a vector mapping the agent kind index to a # vectors of indices, each vector corresponding # to all valid events that can apply to a given agent kind - idx_events_each_kind = [ - [i for (i, e) in enumerate(events) if _haskind(e, kind)] - for kind in agent_kinds - ] + idx_events_each_type = [[i for (i, e) in enumerate(events) if t <: e.types] + for t in agent_types] # initialize vectors for the propensities (they are updated in-place later) - propensities_each_kind = [zeros(length(e)) for e in idx_events_each_kind] + propensities_each_type = [zeros(length(e)) for e in idx_events_each_type] # We loop over all propensities. For those that are functions, we can # update the corresponding propensities entry, which will stay fixed. # For the others, we keep track of the indices of the events whose # propensities is a function. Later on when we compute propensities, # only the indices with propensity <: Function are re-updated! - idx_func_propensities_each_type = [Int[] for _ in idx_events_each_kind] - for i in eachindex(agent_kinds) - propensities_type = propensities_each_kind[i] - for (q, j) in enumerate(idx_events_each_kind[i]) + idx_func_propensities_each_type = [Int[] for _ in idx_events_each_type] + for i in eachindex(agent_types) + propensities_type = propensities_each_type[i] + for (q, j) in enumerate(idx_events_each_type[i]) if events[j].propensity isa Real propensities_type[q] = events[j].propensity else # propensity is a custom function! @@ -203,29 +195,20 @@ function EventQueueABM( # because we use the index of `kind_to_index` to access them. # construct the type - ET,PT,FPT,Q = typeof.(( + ET,PT,FPT,TI,Q = typeof.(( idx_events_each_kind, propensities_each_kind, - idx_func_propensities_each_type, queue + idx_func_propensities_each_type, type_to_idx, queue )) - return EventQueueABM{S,A,C,P,E,R,ET,PT,FPT,Q}( + return EventQueueABM{S,A,C,P,E,R,ET,PT,FPT,TI,Q}( agents, space, properties, rng, Ref(0), Ref(0.0), - events, kind_to_index, idx_events_each_kind, + events, type_to_idx, idx_events_each_kind, propensities_each_kind, idx_func_propensities_each_type, queue, autogenerate_on_add, autogenerate_after_action, ) end -# functions used in the construction of the `EventQueueABM` -function _haskind(e::AgentEvent, kind) - if isnothing(e.kinds) - return true - else - return kind ∈ e.kinds - end -end - ########################################################################################### # %% Adding events to the queue ########################################################################################### @@ -253,22 +236,21 @@ current time of the `model`. function add_event!(agent, model) # TODO: Study type stability of this function events = abmevents(model) # Here, we retrieve the applicable events for the agent and corresponding info - idx = getfield(model, :kind_to_index)[kindof(agent)] - valid_event_idxs = getfield(model, :idx_events_each_kind)[idx] - propensities = getfield(model, :propensities_each_kind)[idx] - func_propensities_idxs = getfield(model, :idx_func_propensities_each_type)[idx] + idx = getfield(model, :type_to_idx)[typeof(agent)] + events_type = getfield(model, :idx_events_each_type)[idx] + propensities_type = getfield(model, :propensities_each_type)[idx] + idx_func_propensities_type = getfield(model, :idx_func_propensities_each_type)[idx] # After, we update the propensity vector # (only the propensities that are custom functions need updating) - for i in func_propensities_idxs - event = events[valid_event_idxs[i]] + for i in idx_func_propensities_type + event = events[events_type[i]] p = event.propensity(agent, model) - propensities[i] = p + propensities_type[i] = p end # Then, select an event based on propensities - event_idx = valid_event_idxs[sample_propensity(abmrng(model), propensities)] - # The time to the event is generated from the selected event + event_idx = events_type[sample_propensity(abmrng(model), propensities_type)] # The time to the event is generated from the selected event selected_event = abmevents(model)[event_idx] - selected_prop = propensities[event_idx] + selected_prop = propensities_type[event_idx] t = selected_event.timing(agent, model, selected_prop) # we then propagate to the direct function add_event!(agent, event_idx, t, model) diff --git a/src/deprecations.jl b/src/deprecations.jl index a938c27ee1..595e76fc4c 100644 --- a/src/deprecations.jl +++ b/src/deprecations.jl @@ -391,3 +391,11 @@ function nearby_agents_exact(a, model, r=1) @warn "`nearby_agents_exact` is deprecated in favor of `nearby_agents(...; search=:exact)`." maxlog=1 return (model[id] for id in nearby_ids(a, model, r; search=:exact)) end + +export @multiagent +macro multiagent(defs) + error("@multiagent was removed from Agents.jl because the underlying package + implementing the backend for it was updated to a much simpler methodology, + refer to the 'Performace Tips' section in the documentation to update your + model to use this new methodology.") +end \ No newline at end of file diff --git a/src/simulations/sample.jl b/src/simulations/sample.jl index 53d21e6c39..f81a14968d 100644 --- a/src/simulations/sample.jl +++ b/src/simulations/sample.jl @@ -104,9 +104,9 @@ function replicate!(agent::AbstractAgent, model; kwargs...) end function copy_agent(agent::A, model, id_new; kwargs...) where {A<:AbstractAgent} - if ismultiagenttype(A) - args = ismultiagentsumtype(A) ? new_args_sum_t(agent, model; kwargs...) : new_args_t(agent, model; kwargs...) - newagent = variant_constructor(agent)(id_new, args...) + if is_sumtype(A) + args = new_args_sum_t(agent, model; kwargs...) + newagent = A(variant(agent)(id_new, args...)) else args = new_args_t(agent, model; kwargs...) newagent = A(id_new, args...) diff --git a/src/submodules/schedulers.jl b/src/submodules/schedulers.jl index 470c9aee6e..df1b4c7836 100644 --- a/src/submodules/schedulers.jl +++ b/src/submodules/schedulers.jl @@ -211,63 +211,4 @@ function (sched::ByType)(model::ABM) return Iterators.flatten(it for it in sched.ids) end -""" - Schedulers.ByKind(agent_kinds; shuffle_kinds = true, shuffle_agents = true) - -A scheduler useful only for mixed agent models using the [`@multiagent`](@ref) macro. -- `agent_kinds` is a `Tuple` of all valid agent kinds e.g. `(:B, :A, :C)`. - -## Keyword arguments - -- `shuffle_kinds = true` groups by agent kind, but randomizes the kind order. - Otherwise returns agent IDs grouped in order of appearance in `agent_kinds`. -- `shuffle_agents = true` randomizes the order of agents within each group, `false` returns - the default order of the container (equivalent to [`Schedulers.fastest`](@ref)). -""" -struct ByKind{K} - shuffle_kinds::Bool - shuffle_agents::Bool - kind_inds::K - ids::Vector{Vector{Int}} -end - -function ByKind(agent_kinds; shuffle_kinds = true, shuffle_agents = true) - return ByKind( - shuffle_kinds, - shuffle_agents, - NamedTuple(t => i for (i, t) in enumerate(agent_kinds)), - [Int[] for _ in 1:length(agent_kinds)] - ) -end - -function (sched::ByKind)(model::ABM) - - n_agents = zeros(Int, length(sched.kind_inds)) - @inbounds for agent in allagents(model) - cont_idx = sched.kind_inds[kindof(agent)] - n_agents[cont_idx] += 1 - curr_idx = n_agents[cont_idx] - ids_kind = sched.ids[cont_idx] - if curr_idx <= length(ids_kind) - ids_kind[curr_idx] = agent.id - else - push!(ids_kind, agent.id) - end - end - - @inbounds for i in 1:length(sched.kind_inds) - resize!(sched.ids[i], n_agents[i]) - end - - sched.shuffle_kinds && shuffle!(abmrng(model), sched.ids) - - @inbounds if sched.shuffle_agents - for i in 1:length(sched.ids) - shuffle!(abmrng(model), sched.ids[i]) - end - end - - return Iterators.flatten(sched.ids) -end - end # Schedulers submodule From 5081b270dfb9120c93af5e5cc7a6482eeeb1d5a1 Mon Sep 17 00:00:00 2001 From: Tortar Date: Sun, 7 Jul 2024 16:23:51 +0200 Subject: [PATCH 02/54] fixes --- CHANGELOG.md | 7 +- docs/Project.toml | 1 + docs/src/index.md | 2 +- docs/src/performance_tips.md | 12 +- docs/src/tutorial.jl | 155 ++++--------- docs/src/tutorial.md | 138 ++++-------- examples/event_rock_paper_scissors.jl | 19 +- src/core/agents.jl | 1 - src/core/model_event_queue.jl | 16 +- src/core/model_standard.jl | 4 +- src/deprecations.jl | 209 +++++++++++++++++- src/simulations/sample.jl | 6 +- src/submodules/schedulers.jl | 58 +++++ .../branching_faster_than_dispatch.jl | 93 +++----- 14 files changed, 415 insertions(+), 306 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55728ecd78..bd48de9090 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,12 +18,7 @@ _We tried to deprecate every major change, resulting in practically no breakage - Every aspect of Agents.jl is orthogonal to `AgentBasedModel`: movement and neighbor searching in any space, data collection, visualizations, etc., are independent of the specific type of `AgentBasedModel` and work out of the box with any model. - Logic of when to collect data in `run!` has been improved to accommodate both discrete and continuous time models. This is reflected in the new options for the keyword `when`. - A new keyword `init` is now available for `run!` to data collect from the model before evolving it. Whether data were collected at time 0 or not was not really obvious in the original version of `run!` due to the ambiguity of the previous handling of `when`. -- A new `@multiagent` macro allows to run multi-agent simulations much more efficiently. It has - two version: In `:opt_speed` the created agents are optimized such as there is virtually - no performance difference between having 1 agent type at the cost of each agent occupying - more memory that in the `Union` case. In `:opt_memory` each agent is optimized to occupy practically - the same memory as the `Union` case, however this comes at a cost of performance versus having 1 type. -- `@multiagent` kinds support multiple dispatch like syntax with the `@dispatch` macro. +- Integration with `DynamicSumTypes.@sumtype` macro allows to run multi-agent simulations much more efficiently. - A new experimental model type `EventQueueABM` has been implemented. It operates in continuous time through the scheduling of events at arbitrary time points, in contrast with the discrete time nature of a `StandardABM`. - Both the visualization and the model abstract interface have been refactored to improve the user diff --git a/docs/Project.toml b/docs/Project.toml index a2fa034cf1..0758cd1e6e 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -9,6 +9,7 @@ ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DiffEqCallbacks = "459566f4-90b8-5000-8ac3-15dfb0a30def" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +DynamicSumTypes = "5fcdbb90-de43-509e-b9a6-c4d43f29cf26" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterTools = "35a29f4d-8980-5a13-9543-d66fff28ecb8" DrWatson = "634d3b9d-ee7a-5ddf-bec9-22491ea816e1" diff --git a/docs/src/index.md b/docs/src/index.md index d98ccefa26..c42de7197b 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -16,7 +16,7 @@ using CairoMakie, Agents Please see the online [CHANGELOG](https://github.com/JuliaDynamics/Agents.jl/blob/main/CHANGELOG.md) for a full list of changes. The most noteworthy ones are: - - A new `@multiagent` macro allows to run multi-agent simulations much more efficiently. + - Integration with `DynamicSumTypes.@sumtype` allows to run multi-agent simulations much more efficiently. - A new experimental model type `EventQueueABM` has been implemented. It operates in continuous time through the scheduling of events at arbitrary time points. It is a generalization of "Gillespie-like" models. - `AgentBasedModel` defines an API that can be extended by other models. - Stronger inheritance capabilities in `@agent`. diff --git a/docs/src/performance_tips.md b/docs/src/performance_tips.md index 0bce225183..820f08f255 100644 --- a/docs/src/performance_tips.md +++ b/docs/src/performance_tips.md @@ -117,17 +117,15 @@ Specifically, you can represent this property as a standard Julia `Array` that i For an example of how this is done, see the [Forest fire](@ref) model, which is a cellular automaton that has no agents in it, or the [Daisyworld](@ref) model, which has both agents as well as a spatial property represented by an `Array`. -## [Multiple agent types: `@multiagent` versus `Union` types](@id multi_vs_union) +## [Multiple agent types: `@sumtype` versus `Union` types](@id sum_vs_union) Due to the way Julia's type system works, and the fact that agents are grouped in a container mapping IDs to agent instances, using a `Union` for different agent types always creates a performance hit because it leads to type instability. On the other hand, a `Union` of different types allows utilizing Julia's multiple dispatch system. -The [`@multiagent`](@ref) macro does not make multiple types. It only makes one large type and defines convenience "constructors" on top of it, giving the illusion that multiple types exist. Therefore it completely eliminates type instability. -[`@multiagent`](@ref) has two versions. In `:opt_speed` the created agents are optimized such as there is virtually no performance difference between having 1 agent type at the cost of each agent occupying more memory that in the `Union` case. -In `:opt_memory` each agent is optimized to occupy practically the same memory as the `Union` case, however this comes at a cost of performance versus having 1 type. +The [`@sumtype`](@ref) macro enclose all types in a single one making working with it type stable. In the following script, which you can find in `test/performance/variable_agent_types_simple_dynamics.jl`, we create a basic money-exchange ABM with many different agent types (up to 15), while having the simulation rules the same regardless of how many agent types are there. -We then compare the performance of the three versions for multiple agent types, incrementally employing more agents from 2 to 15. +We then compare the performance of the two versions for multiple agent types, incrementally employing more agents from 2 to 15. Here are the results of how much time it took to run each version: ```@example performance @@ -138,5 +136,5 @@ include(t) ``` We see that Unions of up to three different Agent types do not suffer much. -Hence, if you have less than four agent types in your model, using different types is still a valid option and allows you to utilize multiple dispatch. -For more agent types however we recommend using the [`@multiagent`](@ref) macro. +Hence, if you have less than four agent types in your model, using different types is still a valid option. +For more agent types however we recommend using the [`@sumtype`](@ref) macro. diff --git a/docs/src/tutorial.jl b/docs/src/tutorial.jl index 8280c2ac76..928310d1ed 100644 --- a/docs/src/tutorial.jl +++ b/docs/src/tutorial.jl @@ -680,12 +680,11 @@ adf # In realistic modelling situations it is often the case the the ABM is composed # of different types of agents. Agents.jl supports two approaches for multi-agent ABMs. -# The first uses the `Union` type, and the second -# uses the [`@multiagent`](@ref) command. `@multiagent` is recommended -# as default, because in many cases it will have performance advantages over the `Union` approach -# without having tangible disadvantages. However, you should read through -# the [comparison of the two approaches](@ref multi_vs_union) to -# be better informed on which one to choose. +# The first uses the `Union` type (this subsection), and the second uses the [`@sumtype`](@ref) +# command (next subsection) available in [DynamicSumTypes.jl](https://github.com/JuliaDynamics/DynamicSumTypes.jl). +# This approach is recommended as default, because in many cases it will have performance advantages +# over the `Union` approach without having tangible disadvantages. However, we strongly recommend you +# to read through the [comparison of the two approaches](@ref sum_vs_union). # _Note that using multiple agent types is a possibility entirely orthogonal to # the type of `AgentBasedModel` or the type of space. Everything we describe here @@ -695,7 +694,7 @@ adf # The simplest way to add more agent types is to make more of them with # [`@agent`](@ref) and then give a `Union` of agent types as the agent type when -# making the `AgentBasedModel`. This is the most "native Julia" approach. +# making the `AgentBasedModel`. # For example, let's say that a new type of agent enters # the simulation; a politician that would "attract" a preferred demographic. @@ -716,12 +715,12 @@ model = StandardABM( # act differently depending on the agent type. This could be done by making # a function that calls other functions depending on the type, such as -function union_step!(agent, model) - if typeof(agent) <: AgentSchelling - schelling_step!(agent, model) - elseif typeof(agent) <: Politician - politician_step!(agent, model) - end +function agent_step!(agent::SchellingAgent, model) + # stuff. +end + +function agent_step!(agent::Politician, model) + # other stuff. end # and then passing @@ -732,129 +731,55 @@ model = StandardABM( agent_step! = union_step! ) -# This approach also works with the [`@multiagent`](@ref) possibility we discuss below. -# `Union` types however also offer the unique possibility of utilizing fully the Julia's -# [multiple dispatch system](https://docs.julialang.org/en/v1/manual/methods/). -# Hence, we can use the same function name and add dispatch to it, such as: - -function dispatch_step!(agent::SchellingAgent, model) - ## stuff. -end - -function dispatch_step!(agent::Politician, model) - ## other stuff. -end - -# and give `dispatch_step!` to the `agent_step!` keyword during model creation. - -# ## Multiple agent types with `@multiagent` - -# [`@multiagent`](@ref) is a macro, and hence not "native Julia" syntax like `Union`, -# however it has been designed to be as similar to [`@agent`](@ref) as possible. -# The syntax to use it is like so: - -@multiagent struct MultiSchelling{X}(GridAgent{2}) - @subagent struct Civilian # can't re-define existing `Schelling` name - mood::Bool = false - group::Int - end - @subagent struct Governor{X} # can't redefine existing `Politician` name - group::Int - influence::X - end -end - -# This macro created three names into scope: - -(MultiSchelling, Civilian, Governor) +# ## Multiple agent types with `@sumtype` -# however, only one of these names is an actual Julia type: +# By using `@sumtype` from `DynamicSumTypes.jl` it is possible to improve the +# computational performance of simulations requiring multiple types, while almost +# everything works the same -fieldnames(MultiSchelling) +using DynamicSumTypes -# that contains all fields of all subtypes without duplication, while +@sumtype MultiSchelling(SchellingAgent, Politician) <: AbstractAgent -fieldnames(Civilian) +# Now when you create instances you will need to enclose them in `MultiSchelling` -# doesn't have any fields. Instead, -# you should think of `Civilian` and `Governor` as just convenience functions that have been -# defined for you to "behave like" types. That's why we call these **kinds** and not -# **types**. +gov = MultiSchelling(Governor(; id = 3 , pos = (2, 2), group = 2, influence = 0.5)) -# E.g., you can initialize - -civ = Civilian(; id = 2, pos = (2, 2), group = 2) # default `mood` - -# or - -gov = Governor(; id = 3 , pos = (2, 2), group = 2, influence = 0.5) - -# exactly as if these were types made with [`@agent`](@ref). -# These are all of type `MultiSchelling` +# agents are then all of type `MultiSchelling` typeof(gov) -# and hence you can't use `typeof` to differentiate them. But you can use - -kindof(gov) - -# instead. - -# Since these kinds are not truly different Julia types, multiple dispatch cannot -# be used to create different agent stepping functions for them. -# The simplest "native Julia" solution here is to create a function where `if` -# clauses decide what to do: +# and hence you can't use only `typeof` to differentiate them. But you can use -function multi_step!(agent, model) - if kindof(agent) == :Civilian - civilian_step!(agent, model) - elseif kindof(agent) == :Governor - politician_step!(agent, model) - end -end - -function civilian_step!(agent, model) - ## stuff. -end +typeof(variant(gov)) -function politician_step!(agent, model) - ## other stuff. -end - -# This however can be made to look much more like multiple dispatch with the -# introduction of another macro, `@dispatch`: +# instead. Hence, the agent stepping function should become something like -@dispatch function multi_step!(agent::Civilian, model) - ## stuff. -end +agent_step!(agent, model) = agent_step!(agent, model, variant(agent)) -@dispatch function multi_step!(agent::Politician, model) - ## other stuff. +function agent_step!(agent, model, ::SchellingAgent) + # stuff. end -# This essentially reconstructs the version previously described with the `if` -# clauses. In general you can use this macro with anything you would dispatch -# on, but this allows also kinds, unlike normal multiple dispatch, for example -# this would also work: - -@dispatch function sub_multi_step!(k::Int, agent::Civilian) - ## some more stuff. +function agent_step!(agent, model, ::Politician) + # other stuff. end -# After we defined the functions with `@dispatch` or the `if` clauses, we can create the model +# and you need to give `MultiSchelling` as the type of agents in model initialization model = StandardABM( - MultiSchelling, # the multi-agent supertype is given as the type + MultiSchelling, # the sum type is given as the type space; agent_step! = multi_step! ) # ## Adding agents of different types to the model -# Regardless of whether you went down the `Union` or `@multiagent` route, +# Regardless of whether you went down the `Union` or `@sumtype` route, # the API of Agents.jl has been designed such that there is no difference in subsequent -# usage. To add agents to a model, we use the existing [`add_agent_single!`](@ref) -# command, but now specifying as a first argument the type of agent to add. +# usage except when adding agents to a model, when doing that with `@sumtype` it is only +# possible to use the existing [`add_agent!`](@ref) command adding an already defined +# agent. # For example, in the union case we provide the `Union` type when we create the model, @@ -872,17 +797,19 @@ add_agent_single!(Politician, model; preferred_demographic = 1) collect(allagents(model)) -# For the `@multiagent` case, there is really no difference. We have +# For the `@sumtype` case instead model = StandardABM(MultiSchelling, space) -# we add +# we add agents like so -add_agent_single!(Civilian, model; group = 1) +agent = MultiSchelling(SchellingAgent(model; pos = random_position(model), group = 1, mood = true)) +add_agent_single!(agent, model) # or -add_agent_single!(Governor, model; influence = 0.5, group = 1) +agent = MultiSchelling(Politician(model; pos = random_position(model), preferred_demographic = 1)) +add_agent_single!(agent, model) # and we see diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index 2af54c503f..bebe47bc28 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -708,11 +708,11 @@ _**And this concludes the main tutorial!**_ In realistic modelling situations it is often the case the the ABM is composed of different types of agents. Agents.jl supports two approaches for multi-agent ABMs. -The first uses the `Union` type (this subsection), and the second -uses the [`@multiagent`](@ref) command (next subsection). `@multiagent` is recommended -as default, because in many cases it will have performance advantages over the `Union` approach -without having tangible disadvantages. However, we strongly recommend you to read through -the [comparison of the two approaches](@ref multi_vs_union). +The first uses the `Union` type (this subsection), and the second uses the [`@sumtype`](@ref) +command (next subsection) available in [DynamicSumTypes.jl](https://github.com/JuliaDynamics/DynamicSumTypes.jl). +This approach is recommended as default, because in many cases it will have performance advantages +over the `Union` approach without having tangible disadvantages. However, we strongly recommend you +to read through the [comparison of the two approaches](@ref sum_vs_union). _Note that using multiple agent types is a possibility entirely orthogonal to the type of `AgentBasedModel` or the type of space. Everything we describe here @@ -742,16 +742,15 @@ model = StandardABM( ```` Naturally, we would have to define a new agent stepping function that would -act differently depending on the agent type. This could be done by making -a function that calls other functions depending on the type, such as +act differently depending on the agent type ````@example tutorial -function union_step!(agent, model) - if typeof(agent) <: AgentSchelling - schelling_step!(agent, model) - elseif typeof(agent) <: Politician - politician_step!(agent, model) - end +function agent_step!(agent::SchellingAgent, model) + # stuff. +end + +function agent_step!(agent::Politician, model) + # other stuff. end ```` @@ -765,105 +764,55 @@ model = StandardABM( ) ```` -This approach also works with the [`@multiagent`](@ref) possibility we discuss below. -`Union` types however also offer the unique possibility of utilizing Julia's -[multiple dispatch system](https://docs.julialang.org/en/v1/manual/methods/). -Hence, we can use the same function name and add dispatch to it, such as: - -````@example tutorial -function dispatch_step!(agent::SchellingAgent, model) - # stuff. -end - -function dispatch_step!(agent::Politician, model) - # other stuff. -end -```` - -and give `dispatch_step!` to the `agent_step!` keyword during model creation. - -## Multiple agent types with `@multiagent` - -[`@multiagent`](@ref) does not offer the multiple dispatch possibility, but in the -majority of cases leads to better computational performance. Intentionally -the command has been designed to be as similar to [`@agent`](@ref) as possible. -The syntax to use it is like so: - -````@example tutorial -@multiagent struct MultiSchelling{X}(GridAgent{2}) - @subagent struct Civilian # can't re-define existing `Schelling` name - mood::Bool = false - group::Int - end - @subagent struct Governor{X} # can't redefine existing `Politician` name - group::Int - influence::X - end -end -```` - -This macro created three names into scope: - -````@example tutorial -(MultiSchelling, Civilian, Governor) -```` +## Multiple agent types with `@sumtype` -however, only one of these names is an actual Julia type: +By using `@sumtype` from `DynamicSumTypes.jl` it is possible to improve the +computational performance of simulations requiring multiple types, while almost +everything works the same ````@example tutorial -fieldnames(MultiSchelling) -```` +using DynamicSumTypes -that contains all fields of all subtypes without duplication, while - -````@example tutorial -fieldnames(Civilian) +@sumtype MultiSchelling(SchellingAgent, Politician) <: AbstractAgent ```` -doesn't have any fields. Instead, -you should think of `Civilian` and `Governor` as just convenience functions that have been -defined for you to "behave like" types. E.g., you can initialize +Now when you create instances you will need to enclose them in `MultiSchelling` ````@example tutorial -civ = Civilian(; id = 2, pos = (2, 2), group = 2) # default `mood` +gov = MultiSchelling(Governor(; id = 3 , pos = (2, 2), group = 2, influence = 0.5)) ```` -or +agents are then all of type `MultiSchelling` ````@example tutorial -gov = Governor(; id = 3 , pos = (2, 2), group = 2, influence = 0.5) +typeof(gov) ```` -exactly as if these were types made with [`@agent`](@ref). -These are all of type `MultiSchelling` +and hence you can't use only `typeof` to differentiate them. But you can use ````@example tutorial -typeof(gov) +typeof(variant(gov)) ```` -and hence you can't use `typeof` to differentiate them. But you can use +instead. Hence, the agent stepping function should become something like ````@example tutorial -kindof(gov) -```` +agent_step!(agent, model) = agent_step!(agent, model, variant(agent)) -instead. Hence, the agent stepping function should be something like +function agent_step!(agent, model, ::SchellingAgent) + # stuff. +end -````@example tutorial -function multi_step!(agent, model) - if kindof(agent) == :Civilian - schelling_step!(agent, model) - elseif kindof(agent) == :Governor - politician_step!(agent, model) - end +function agent_step!(agent, model, ::Politician) + # other stuff. end ```` -and give that to the model creation +and you need to give `MultiSchelling` as the type of agents in model initialization ````@example tutorial model = StandardABM( - MultiSchelling, # the multi-agent supertype is given as the type + MultiSchelling, # the sum type is given as the type space; agent_step! = multi_step! ) @@ -871,10 +820,11 @@ model = StandardABM( ## Adding agents of different types to the model -Regardless of whether you went down the `Union` or `@multiagent` route, +Regardless of whether you went down the `Union` or `@sumtype` route, the API of Agents.jl has been designed such that there is no difference in subsequent -usage. To add agents to a model, we use the existing [`add_agent!`](@ref) -command, but now specifying as a first argument the type of agent to add. +usage except when adding agents to a model, when doing that with `@sumtype` it is only +possible to use the existing infrastructure for adding agents with an already defined +agent. For example, in the union case we provide the `Union` type when we create the model, @@ -885,13 +835,13 @@ model = StandardABM(Union{SchellingAgent, Politician}, space) we add them by specifying the type ````@example tutorial -add_agent!(SchellingAgent, model; group = 1, mood = true) +add_agent_single!(SchellingAgent, model; group = 1, mood = true) ```` or ````@example tutorial -add_agent!(Politician, model; preferred_demographic = 1) +add_agent_single!(Politician, model; preferred_demographic = 1) ```` and we see @@ -900,22 +850,24 @@ and we see collect(allagents(model)) ```` -For the `@multiagent` case, there is really no difference. We have +For the `@sumtype` case instead ````@example tutorial model = StandardABM(MultiSchelling, space) ```` -we add +we add agents like so ````@example tutorial -add_agent!(Civilian, model; group = 1) +agent = MultiSchelling(SchellingAgent(model; pos = random_position(model), group = 1, mood = true)) +add_agent_single!(agent, model) ```` or ````@example tutorial -add_agent!(Governor, model; influence = 0.5, group = 1) +agent = MultiSchelling(Politician(model; pos = random_position(model), preferred_demographic = 1)) +add_agent_single!(agent, model) ```` and we see diff --git a/examples/event_rock_paper_scissors.jl b/examples/event_rock_paper_scissors.jl index 8859ad26a0..66c9879418 100644 --- a/examples/event_rock_paper_scissors.jl +++ b/examples/event_rock_paper_scissors.jl @@ -155,7 +155,7 @@ end movement_event = AgentEvent( action! = move!, propensity = movement_propensity, - kinds = (:Scissors, :Paper), timing = movement_time + types = (Scissors, Paper), timing = movement_time ) # we wrap all events in a tuple and we are done with the setting up part! @@ -181,8 +181,10 @@ model = EventQueueABM(RPS, events, space; rng, warn = false) # By default, when an agent is added to the model # an event is also generated for it and added to the queue. +const alltypes = (Rock, Paper, Scissors) + for p in positions(model) - type = rand(abmrng(model), (Rock, Paper, Scissors)) + type = rand(abmrng(model), alltypes) add_agent!(p, type, model) end @@ -205,7 +207,7 @@ function initialize_rps(; n = 100, nx = n, ny = n, seed = 42) rng = Xoshiro(seed) model = EventQueueABM(RPS, events, space; rng, warn = false) for p in positions(model) - type = rand(abmrng(model), (Rock, Paper, Scissors)) + type = rand(abmrng(model), alltypes) add_agent!(p, type, model) end return model @@ -227,13 +229,12 @@ nagents(model) # than a threshold function terminate(model, t) - kinds = allkinds(RPS) threshold = 1000 ## Alright, this code snippet loops over all kinds, ## and for each it checks if it is less than the threshold. ## if any is, it returns `true`, otherwise `false.` - logic = any(kinds) do kind - n = count(a -> kindof(a) == kind, allagents(model)) + logic = any(alltypes) do type + n = count(a -> typeof(a) == type, allagents(model)) return n < threshold end ## For safety, in case this never happens, we also add a trigger @@ -258,7 +259,7 @@ abmtime(model) model = initialize_rps() -adata = [(a -> kindof(a) === X, count) for X in allkinds(RPS)] +adata = [(a -> typeof(a) === X, count) for X in alltypes] adf, mdf = run!(model, 100.0; adata, when = 0.5, dt = 0.01) @@ -288,8 +289,8 @@ fig # Naturally, for `EventQueueABM` the `dt` argument of [`abmvideo`](@ref) # corresponds to continuous time and does not have to be an integer. -const colormap = Dict(:Rock => "black", :Scissors => "gray", :Paper => "orange") -agent_color(agent) = colormap[kindof(agent)] +const colormap = Dict(Rock => "black", Scissors => "gray", Paper => "orange") +agent_color(agent) = colormap[typeof(agent)] plotkw = (agent_color, agent_marker = :rect, agent_size = 5) fig, ax, abmobs = abmplot(model; plotkw...) diff --git a/src/core/agents.jl b/src/core/agents.jl index 47dfbe95fb..31dc2e006d 100644 --- a/src/core/agents.jl +++ b/src/core/agents.jl @@ -50,7 +50,6 @@ as well as any additional ones the user may provide. The macro supports all synt that the standard Julia `mutable struct` command allows for, such as `const` field declaration or default values for some fields. Additionally, the resulting type will always have a keyword constructor defined for it (using `@kwdef`). -See below for examples and see also [`@multiagent`](@ref). Using `@agent` is the recommended way to create agent types for Agents.jl. diff --git a/src/core/model_event_queue.jl b/src/core/model_event_queue.jl index 73e0fa0f4e..bfeec7bfa3 100644 --- a/src/core/model_event_queue.jl +++ b/src/core/model_event_queue.jl @@ -111,12 +111,10 @@ Here is how to construct an `EventQueueABM`: EventQueueABM(AgentType, events [, space]; kwargs...) Create an instance of an [`EventQueueABM`](@ref). -`AgentType` is a _single_ agent type representing the agents that participate -in the simulation. Unlike [`StandardABM`](@ref), `EventQueueABM` does not support -`Union` agent types for multi-agent simulations (because multiple dispatch is not -intended to be used to choose events, see the `events` argument below). -Only the [`@multiagent`](@ref) macro is supported and agent "kinds" should be -compared with the [`kindof`](@ref) function as instructed in the main [Tutorial](@ref). + +The model expects agents of type `AgentType(s)` living in the given `space`. +`AgentType(s)` is the result of [`@agent`](@ref) or `@sumtype` or +a `Union` of agent types. `space` is a subtype of `AbstractSpace`, see [Space](@ref Space) for all available spaces. @@ -196,15 +194,15 @@ function EventQueueABM( # construct the type ET,PT,FPT,TI,Q = typeof.(( - idx_events_each_kind, propensities_each_kind, + idx_events_each_type, propensities_each_type, idx_func_propensities_each_type, type_to_idx, queue )) return EventQueueABM{S,A,C,P,E,R,ET,PT,FPT,TI,Q}( agents, space, properties, rng, Ref(0), Ref(0.0), - events, type_to_idx, idx_events_each_kind, - propensities_each_kind, idx_func_propensities_each_type, + events, type_to_idx, idx_events_each_type, + propensities_each_type, idx_func_propensities_each_type, queue, autogenerate_on_add, autogenerate_after_action, ) end diff --git a/src/core/model_standard.jl b/src/core/model_standard.jl index dbe89a694a..2d32db017a 100644 --- a/src/core/model_standard.jl +++ b/src/core/model_standard.jl @@ -54,7 +54,7 @@ To construct a `StandardABM` use the syntax: StandardABM(AgentType(s) [, space]; properties, agent_step!, model_step!, kwargs...) The model expects agents of type `AgentType(s)` living in the given `space`. -`AgentType(s)` is the result of [`@agent`](@ref), [`@multiagent`](@ref) or +`AgentType(s)` is the result of [`@agent`](@ref) or `@sumtype` or a `Union` of agent types. `space` is a subtype of `AbstractSpace`, see [Space](@ref Space) for all available spaces. @@ -152,7 +152,7 @@ function StandardABM( ABMObservable. """ maxlog=1 end - !(ismultiagenttype(A)) && agent_validator(A, space, warn) + !(ismultiagenttype(A)) && !(is_sumtype(A)) && agent_validator(A, space, warn) C = construct_agent_container(container, A) agents = C() agents_types = union_types(A) diff --git a/src/deprecations.jl b/src/deprecations.jl index 595e76fc4c..2c2e3d0b45 100644 --- a/src/deprecations.jl +++ b/src/deprecations.jl @@ -392,10 +392,207 @@ function nearby_agents_exact(a, model, r=1) return (model[id] for id in nearby_ids(a, model, r; search=:exact)) end -export @multiagent -macro multiagent(defs) - error("@multiagent was removed from Agents.jl because the underlying package - implementing the backend for it was updated to a much simpler methodology, - refer to the 'Performace Tips' section in the documentation to update your - model to use this new methodology.") +export @multiagent, @dispatch, @finalize_dispatch, kindof, allkinds +export DynamicSumTypes +using DynamicSumTypes: allkinds + +""" + @multiagent struct YourAgentType{X,Y}(AgentTypeToInherit) [<: OptionalSupertype] + @subagent FirstAgentSubType{X} + first_property::X # shared with second agent + second_property_with_default::Bool = true + end + @subagent SecondAgentSubType{X,Y} + first_property::X = 3 + third_property::Y + end + # etc... + end +Define multiple agent "subtypes", which are actually only variants of a unique +overarching type `YourAgentType`. This means that all "subtypes" are conceptual: they are simply +convenience functions defined that initialize the common proper type correctly +(see examples below for more). Because the "subtypes" are not real Julia `Types`, +you cannot use multiple dispatch on them. You also cannot distinguish them +on the basis of `typeof`, but need to use instead the [`kindof`](@ref) function. +That is why these "types" are often referred to as "kinds" in the documentation. +See also the [`allkinds`](@ref) function for a convenient way to obtain all kinds. +See the [Tutorial](@ref) or the [performance comparison versus `Union` types](@ref sum_vs_union) +for why in most cases it is better to use `@multiagent` than making multiple +agent types manually. See [`@dispatch`](@ref) (also highlighted in the [Tutorial](@ref)) +for a multiple-dispatch-like syntax to use with `@multiagent`. +Two different versions of `@multiagent` can be used by passing either `:opt_speed` or +`:opt_memory` as the first argument (before the `struct` keyword). +The first optimizes the agents representation for +speed, the second does the same for memory, at the cost of a moderate drop in performance. +By default it uses `:opt_speed`. +## Examples +Let's say you have this definition: +``` +@multiagent :opt_speed struct Animal{T}(GridAgent{2}) + @subagent struct Wolf + energy::Float64 = 0.5 + ground_speed::Float64 + const fur_color::Symbol + end + @subagent struct Hawk{T} + energy::Float64 = 0.1 + ground_speed::Float64 + flight_speed::T + end +end +``` +Then you can create `Wolf` and `Hawk` agents normally, like so +``` +hawk_1 = Hawk(1, (1, 1), 1.0, 2.0, 3) +hawk_2 = Hawk(; id = 2, pos = (1, 2), ground_speed = 2.3, flight_speed = 2) +wolf_1 = Wolf(3, (2, 2), 2.0, 3.0, :black) +wolf_2 = Wolf(; id = 4, pos = (2, 1), ground_speed = 2.0, fur_color = :white) +``` +It is important to notice, though, that the `Wolf` and `Hawk` types are just +conceptual and all agents are actually of type `Animal` in this case. +The way to retrieve the variant of the agent is through the function `kindof` e.g. +``` +kindof(hawk_1) # :Hawk +kindof(wolf_2) # :Wolf +``` +See the [rabbit_fox_hawk](@ref) example to see how to use this macro in a model. +## Current limitations +- Impossibility to inherit from a compactified agent. +""" +macro multiagent(version, struct_repr) + expr = _multiagent(version, struct_repr) + return esc(expr) +end + +macro multiagent(struct_repr) + @warn "@multiagent is deprecated because the underlying package + implementing the backend for it was updated to a much simpler methodology, + refer to the updated Tutorial in the documentation to update your + model to use this new methodology." + expr = _multiagent(QuoteNode(:opt_speed), struct_repr) + return esc(expr) +end + +function _multiagent(version, struct_repr) + new_type, base_type_spec, abstract_type, agent_specs = decompose_struct_base(struct_repr) + base_fields = compute_base_fields(base_type_spec) + agent_specs_with_base = [] + for a_spec in agent_specs + @capture(a_spec, @subagent astruct_spec_) + int_type, new_fields = decompose_struct(astruct_spec) + push!(agent_specs_with_base, + :(@kwdef mutable struct $int_type + $(base_fields...) + $(new_fields...) + end)) + end + t = :($new_type <: $abstract_type) + c = @capture(new_type, new_type_n_{new_params__}) + if c == false + new_type_n = new_type + new_params = [] + end + new_params_no_constr = [p isa Expr && p.head == :(<:) ? p.args[1] : p for p in new_params] + new_type_no_constr = :($new_type_n{$(new_params_no_constr...)}) + a_specs = :(begin $(agent_specs_with_base...) end) + if version == QuoteNode(:opt_speed) + expr = quote + DynamicSumTypes.@sum_structs :on_fields $t $a_specs + Agents.ismultiagentcompacttype(::Type{$(namify(new_type))}) = true + DynamicSumTypes.export_variants($(namify(t))) + end + elseif version == QuoteNode(:opt_memory) + expr = quote + DynamicSumTypes.@sum_structs :on_types $t $a_specs + Agents.ismultiagentsumtype(::Type{$(namify(new_type))}) = true + DynamicSumTypes.export_variants($(namify(t))) + end + else + error("The version of @multiagent chosen was not recognized, use either `:opt_speed` or `:opt_memory` instead.") + end + + expr_multiagent = :(Agents.ismultiagenttype(::Type{$(namify(new_type))}) = true) + if new_params != [] + if version == QuoteNode(:opt_speed) + expr_multiagent_p = quote + Agents.ismultiagenttype(::Type{$(new_type_no_constr)}) where {$(new_params...)} = true + Agents.ismultiagentcompacttype(::Type{$(new_type_no_constr)}) where {$(new_params...)} = true + end + else + expr_multiagent_p = quote + Agents.ismultiagenttype(::Type{$(new_type_no_constr)}) where {$(new_params...)} = true + Agents.ismultiagentsumtype(::Type{$(new_type_no_constr)}) where {$(new_params...)} = true + end + end + else + expr_multiagent_p = :() + end + + expr = quote + $expr + $expr_multiagent + $expr_multiagent_p + nothing + end + + return expr +end + +# This function is extended in the `@multiagent` macro +ismultiagenttype(::Type) = false +ismultiagentsumtype(::Type) = false +ismultiagentcompacttype(::Type) = false + +""" + kindof(agent::AbstractAgent) → kind::Symbol +Return the "kind" (instead of type) of the agent, which is the name given to the +agent subtype when it was created with [`@multiagent`](@ref). +""" +function DynamicSumTypes.kindof(a::AbstractAgent) + throw(ArgumentError("Agent of type $(typeof(a)) has not been created via `@multiagent`.")) +end + +""" + allkinds(AgentType::Type) → kinds::Tuple +Return all "kinds" that compose the given agent type that was generated via +the [`@multiagent`](@ref) macro. The kinds are returned as a tuple of `Symbol`s. +""" +function DynamicSumTypes.allkinds(a::Type{<:AbstractAgent}) + (nameof(a), ) # this function is extended automatically in the macro +end + +""" + @dispatch f(args...) +A macro to enable multiple-dispatch-like behavior for the function `f`, +for various agent kinds generated via the [`@multiagent`](@ref) macro. +For an illustration of its usage see the [Tutorial](@ref). +If you are creating your own module/package that uses Agents.jl, and you +are using `@dispatch` inside it, then you need to put [`@finalize_dispatch`](@ref)`()` +before the module end (but after all `@dispatch` calls). +""" +macro dispatch(f_def) + return esc(:(DynamicSumTypes.@pattern $f_def)) +end + +""" + @finalize_dispatch +A macro to finalize the definitions of the methods generated by the +`@dispatch` macro when used in a module. It just needs to be used +once at the end of the module if no `@dispatch` method is used inside +of it. Otherwise use it also before the invocations of the methods. +## Examples +```julia +module SomeModel +@multiagent struct MultiAgent(NoSpaceAgent) + @subagent SubAgent1 end + @subagent SubAgent2 end +end +@dispatch MethodSub1(::SubAgent1) = 1 +@dispatch MethodSub1(::SubAgent2) = 1 +@finalize_dispatch() +end +``` +""" +macro finalize_dispatch() + return esc(:(DynamicSumTypes.@finalize_patterns)) end \ No newline at end of file diff --git a/src/simulations/sample.jl b/src/simulations/sample.jl index f81a14968d..0f029dbc03 100644 --- a/src/simulations/sample.jl +++ b/src/simulations/sample.jl @@ -104,7 +104,11 @@ function replicate!(agent::AbstractAgent, model; kwargs...) end function copy_agent(agent::A, model, id_new; kwargs...) where {A<:AbstractAgent} - if is_sumtype(A) + # TODO: this first if statement should be removed as soon as @multiagent is removed + if ismultiagenttype(A) + args = ismultiagentsumtype(A) ? new_args_sum_t(agent, model; kwargs...) : new_args_t(agent, model; kwargs...) + newagent = variant_constructor(agent)(id_new, args...) + elseif is_sumtype(A) args = new_args_sum_t(agent, model; kwargs...) newagent = A(variant(agent)(id_new, args...)) else diff --git a/src/submodules/schedulers.jl b/src/submodules/schedulers.jl index df1b4c7836..821fe16e0b 100644 --- a/src/submodules/schedulers.jl +++ b/src/submodules/schedulers.jl @@ -211,4 +211,62 @@ function (sched::ByType)(model::ABM) return Iterators.flatten(it for it in sched.ids) end +# TODO: ByKind scheduler should be removed when deprecation of @multiagent is complete + +""" + Schedulers.ByKind(agent_kinds; shuffle_kinds = true, shuffle_agents = true) +A scheduler useful only for mixed agent models using the [`@multiagent`](@ref) macro. +- `agent_kinds` is a `Tuple` of all valid agent kinds e.g. `(:B, :A, :C)`. +## Keyword arguments +- `shuffle_kinds = true` groups by agent kind, but randomizes the kind order. + Otherwise returns agent IDs grouped in order of appearance in `agent_kinds`. +- `shuffle_agents = true` randomizes the order of agents within each group, `false` returns + the default order of the container (equivalent to [`Schedulers.fastest`](@ref)). +""" +struct ByKind{K} + shuffle_kinds::Bool + shuffle_agents::Bool + kind_inds::K + ids::Vector{Vector{Int}} +end + +function ByKind(agent_kinds; shuffle_kinds = true, shuffle_agents = true) + return ByKind( + shuffle_kinds, + shuffle_agents, + NamedTuple(t => i for (i, t) in enumerate(agent_kinds)), + [Int[] for _ in 1:length(agent_kinds)] + ) +end + +function (sched::ByKind)(model::ABM) + + n_agents = zeros(Int, length(sched.kind_inds)) + @inbounds for agent in allagents(model) + cont_idx = sched.kind_inds[kindof(agent)] + n_agents[cont_idx] += 1 + curr_idx = n_agents[cont_idx] + ids_kind = sched.ids[cont_idx] + if curr_idx <= length(ids_kind) + ids_kind[curr_idx] = agent.id + else + push!(ids_kind, agent.id) + end + end + + @inbounds for i in 1:length(sched.kind_inds) + resize!(sched.ids[i], n_agents[i]) + end + + sched.shuffle_kinds && shuffle!(abmrng(model), sched.ids) + + @inbounds if sched.shuffle_agents + for i in 1:length(sched.ids) + shuffle!(abmrng(model), sched.ids[i]) + end + end + + return Iterators.flatten(sched.ids) +end + end # Schedulers submodule diff --git a/test/performance/branching_faster_than_dispatch.jl b/test/performance/branching_faster_than_dispatch.jl index 9e0354b467..a4bde8f87a 100644 --- a/test/performance/branching_faster_than_dispatch.jl +++ b/test/performance/branching_faster_than_dispatch.jl @@ -1,8 +1,8 @@ # This file compares approaching multi-agent models in two ways: # 1) using different Types to represent different agents. This leads to type -# stability within `step!`. -# 2) using a single type with some extra property `type` or `kind`, and then do -# an `if`-based branching on this type to dispatch to different functions. +# instabilities. +# 2) using @sumtype to enclose all types in a single one. This removes type +# instabilities. # The result is that (2) is much faster. @@ -89,54 +89,25 @@ end ################### DEFINITION 2 ############### -@multiagent :opt_speed struct GridAgent2All(GridAgent{2}) - @subagent struct GridAgent2One - one::Float64 - two::Bool - three::Int - end - @subagent struct GridAgent2Two - one::Float64 - two::Bool - four::Float64 - end - @subagent struct GridAgent2Three - one::Float64 - two::Bool - five::Bool - end - @subagent struct GridAgent2Four - one::Float64 - two::Bool - six::Int16 - end - @subagent struct GridAgent2Five - one::Float64 - two::Bool - seven::Int32 - end - @subagent struct GridAgent2Six - one::Float64 - two::Bool - eight::Int64 - end -end +using DynamicSumTypes -@dispatch agent_step!(agent::GridAgent2One, model2) = randomwalk!(agent, model2) -@dispatch function agent_step!(agent::GridAgent2Two, model2) +agent_step!(agent, model2) = agent_step!(agent, model2, variant(agent)) + +agent_step!(agent, model2, ::GridAgentOne) = randomwalk!(agent, model2) +function agent_step!(agent, model2, ::GridAgentTwo) agent.one += rand(abmrng(model2)) agent.two = rand(abmrng(model2), Bool) end -@dispatch function agent_step!(agent::GridAgent2Three, model2) - if any(a-> kindof(a) == :gridagenttwo, nearby_agents(agent, model2)) +function agent_step!(agent, model2, ::GridAgentThree) + if any(a-> variant(a) isa GridAgentTwo, nearby_agents(agent, model2)) agent.two = true randomwalk!(agent, model2) end end -@dispatch function agent_step!(agent::GridAgent2Four, model2) +function agent_step!(agent, model2, ::GridAgentFour) agent.one += sum(a.one for a in nearby_agents(agent, model2)) end -@dispatch function agent_step!(agent::GridAgent2Five, model2) +function agent_step!(agent, model2, ::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)) @@ -144,12 +115,16 @@ end walk!(agent, sign.(farthest.pos .- agent.pos), model2) end end -@dispatch function agent_step!(agent::GridAgent2Six, model2) +function agent_step!(agent, model2, ::GridAgentSix) agent.eight += sum(rand(abmrng(model2), (0, 1)) for a in nearby_agents(agent, model2)) end +@sumtype GridAgentAll( + GridAgentOne, GridAgentTwo, GridAgentThree, GridAgentFour, GridAgentFive, GridAgentSix +) <: AbstractAgent + model2 = StandardABM( - GridAgent2All, + GridAgentAll, GridSpace((15, 15)); agent_step!, rng = MersenneTwister(42), @@ -157,28 +132,32 @@ model2 = StandardABM( ) for i in 1:500 - add_agent!(GridAgent2One, model2, rand(abmrng(model2)), rand(abmrng(model2), Bool), i) - add_agent!(GridAgent2Two, model2, rand(abmrng(model2)), rand(abmrng(model2), Bool), Float64(i)) - add_agent!(GridAgent2Three, model2, rand(abmrng(model2)), rand(abmrng(model2), Bool), true) - add_agent!(GridAgent2Four, model2, rand(abmrng(model2)), rand(abmrng(model2), Bool), Int16(i)) - add_agent!(GridAgent2Five, model2, rand(abmrng(model2)), rand(abmrng(model2), Bool), Int32(i)) - add_agent!(GridAgent2Six, model2, rand(abmrng(model2)), rand(abmrng(model2), Bool), i) + agent = GridAgentAll(GridAgentOne(model2, random_position(model2), rand(abmrng(model2)), rand(abmrng(model2), Bool), i)) + add_agent_own_pos!(agent, model2) + agent = GridAgentAll(GridAgentTwo(model2, random_position(model2), rand(abmrng(model2)), rand(abmrng(model2), Bool), Float64(i))) + add_agent_own_pos!(agent, model2) + agent = GridAgentAll(GridAgentThree(model2, random_position(model2), rand(abmrng(model2)), rand(abmrng(model2), Bool), true)) + add_agent_own_pos!(agent, model2) + agent = GridAgentAll(GridAgentFour(model2, random_position(model2), rand(abmrng(model2)), rand(abmrng(model2), Bool), Int16(i))) + add_agent_own_pos!(agent, model2) + agent = GridAgentAll(GridAgentFive(model2, random_position(model2), rand(abmrng(model2)), rand(abmrng(model2), Bool), Int32(i))) + add_agent_own_pos!(agent, model2) + agent = GridAgentAll(GridAgentSix(model2, random_position(model2), rand(abmrng(model2)), rand(abmrng(model2), Bool), i)) + add_agent_own_pos!(agent, model2) end ################### Benchmarks ############### @btime step!($model1, 50) -@btime step!($model2, 50) # repeat also with :opt_memory +@btime step!($model2, 50) # Results: -# multiple types: 3.732 s (39242250 allocations: 2.45 GiB) -# @multiagent :opt_speed: 577.185 ms (25818000 allocations: 1.05 GiB) -# @multiagent :opt_memory: 870.460 ms (25868000 allocations: 1.05 GiB) +# multiple types: 3.778 s (38740900 allocations: 2.38 GiB) +# @sumtype: 545.119 ms (22952850 allocations: 965.93 MiB) Base.summarysize(model1) -Base.summarysize(model2) # repeat also with :opt_memory +Base.summarysize(model2) # Results: -# multiple types: 491.20 KiB -# @multiagent :opt_speed: 686.13 KiB -# @multiagent :opt_memory: 563.12 KiB +# multiple types: 543.496 KiB +# @sumtype: 546.360 KiB From e6266aa1be3a5a4440bdaebdaee16a8a3a5e9910 Mon Sep 17 00:00:00 2001 From: Tortar Date: Sun, 7 Jul 2024 17:22:53 +0200 Subject: [PATCH 03/54] update other benchmark --- .../variable_agent_types_simple_dynamics.jl | 93 +++++++------------ 1 file changed, 31 insertions(+), 62 deletions(-) diff --git a/test/performance/variable_agent_types_simple_dynamics.jl b/test/performance/variable_agent_types_simple_dynamics.jl index 1d6c970eeb..fdd5a844a4 100644 --- a/test/performance/variable_agent_types_simple_dynamics.jl +++ b/test/performance/variable_agent_types_simple_dynamics.jl @@ -6,7 +6,7 @@ # are split in a varying number of agents. It shows how much of a # performance hit is to have many different agent types. -using Agents, Random, BenchmarkTools +using Agents, DynamicSumTypes, Random, BenchmarkTools @agent struct Agent1(GridAgent{2}) money::Int @@ -68,27 +68,14 @@ end money::Int end -agents_m = [Symbol(:AgentAllMemory, :($y)) for y in [2,3,4,5,10,15]] -subagents_m = [[Symbol(:Agent, :($x), :m, :($y)) for x in 1:y] for y in [2,3,4,5,10,15]] -expr_subagents_m = [[:(@subagent struct $(Symbol(:Agent, :($x), :m, :($y))) - money::Int - end) for x in 1:y] for y in [2,3,4,5,10,15]] -for (a, subs) in zip(agents_m, expr_subagents_m) - @eval @multiagent :opt_memory struct $a(GridAgent{2}) - $(subs...) - end -end - -agents_s = [Symbol(:AgentAllSpeed, :($y)) for y in [2,3,4,5,10,15]] -subagents_s = [[Symbol(:Agent, :($x), :s, :($y)) for x in 1:y] for y in [2,3,4,5,10,15]] -expr_subagents_s = [[:(@subagent struct $(Symbol(:Agent, :($x), :s, :($y))) - money::Int - end) for x in 1:y] for y in [2,3,4,5,10,15]] -for (a, subs) in zip(agents_s, expr_subagents_s) - @eval @multiagent :opt_speed struct $a(GridAgent{2}) - $(subs...) - end -end +@sumtype AgentAll2(Agent1, Agent2) <: AbstractAgent +@sumtype AgentAll3(Agent1, Agent2, Agent3) <: AbstractAgent +@sumtype AgentAll4(Agent1, Agent2, Agent3, Agent4) <: AbstractAgent +@sumtype AgentAll5(Agent1, Agent2, Agent3, Agent4, Agent5) <: AbstractAgent +@sumtype AgentAll10(Agent1, Agent2, Agent3, Agent4, Agent5, Agent6, + Agent7, Agent8, Agent9, Agent10) <: AbstractAgent +@sumtype AgentAll15(Agent1, Agent2, Agent3, Agent4, Agent5, Agent6, + Agent7, Agent8, Agent9, Agent10, Agent11, Agent12, Agent13, Agent14, Agent15) <: AbstractAgent function initialize_model_1(;n_agents=600,dims=(5,5)) space = GridSpace(dims) @@ -102,33 +89,23 @@ function initialize_model_1(;n_agents=600,dims=(5,5)) return model end -function initialize_model_multi_memory(;n_agents=600, n_types=1, dims=(5,5)) - i = findfirst(x -> length(x) == n_types, subagents_m) - agents_used = [eval(sa) for sa in subagents_m[i]] - space = GridSpace(dims) - model = StandardABM(eval(agents_m[i]), space; agent_step!, - scheduler=Schedulers.Randomly(), warn=false, - rng = Xoshiro(42)) - agents_per_type = div(n_agents, n_types) - for A in agents_used - for _ in 1:agents_per_type - add_agent!(A, model, 10) - end - end - return model -end - -function initialize_model_multi_speed(;n_agents=600, n_types=1, dims=(5,5)) - i = findfirst(x -> length(x) == n_types, subagents_s) - agents_used = [eval(sa) for sa in subagents_s[i]] +function initialize_model_sum(;n_agents=600, n_types=1, dims=(5,5)) + agent_types = [Agent1,Agent2,Agent3,Agent4,Agent5,Agent6,Agent7,Agent8, + Agent9,Agent10,Agent11,Agent12,Agent13,Agent14,Agent15] + agents_used = agent_types[1:n_types] + agent_all_t = Dict(2 => AgentAll2, 3 => AgentAll3, + 4 => AgentAll4, 5 => AgentAll5, + 10 => AgentAll10, 15 => AgentAll15) + agent_all = agent_all_t[n_types] space = GridSpace(dims) - model = StandardABM(eval(agents_s[i]), space; agent_step!, + model = StandardABM(agent_all, space; agent_step!, scheduler=Schedulers.Randomly(), warn=false, rng = Xoshiro(42)) agents_per_type = div(n_agents, n_types) for A in agents_used for _ in 1:agents_per_type - add_agent!(A, model, 10) + agent = agent_all(A(model, random_position(model), 10)) + add_agent_own_pos!(agent, model) end end return model @@ -178,12 +155,8 @@ function run_simulation_1(n_steps) Agents.step!(model, n_steps) end -function run_simulation_multi_memory(n_steps; n_types) - model = initialize_model_multi_memory(; n_types=n_types) - Agents.step!(model, n_steps) -end -function run_simulation_multi_speed(n_steps; n_types) - model = initialize_model_multi_speed(; n_types=n_types) +function run_simulation_sum(n_steps; n_types) + model = initialize_model_sum(; n_types=n_types) Agents.step!(model, n_steps) end @@ -198,33 +171,29 @@ n_types = [2,3,4,5,10,15] time_1 = @belapsed run_simulation_1($n_steps) times_n = Float64[] -times_multi_m = Float64[] times_multi_s = Float64[] for n in n_types println(n) t = @belapsed run_simulation_n($n_steps; n_types=$n) push!(times_n, t/time_1) - t_multi = @belapsed run_simulation_multi_memory($n_steps; n_types=$n) - push!(times_multi_m, t_multi/time_1) - t_multi_speed = @belapsed run_simulation_multi_speed($n_steps; n_types=$n) - push!(times_multi_s, t_multi_speed/time_1) + t_sum = @belapsed run_simulation_sum($n_steps; n_types=$n) + print(t/time_1, " ", t_sum/time_1) + push!(times_multi_s, t_sum/time_1) end -println("relative time of model with 1 type: 1") -for (n, t1, t2, t3) in zip(n_types, times_n, times_multi_m, times_multi_s) +println("relative time of model with 1 type: 1.0") +for (n, t1, t2) in zip(n_types, times_n, times_multi_s) println("relative time of model with $n types: $t1") - println("relative time of model with $n @multiagent :opt_memory: $t2") - println("relative time of model with $n @multiagent :opt_speed: $t3") + println("relative time of model with $n @sumtype: $t2") end using CairoMakie fig, ax = CairoMakie.scatterlines(n_types, times_n; label = "Union"); -scatterlines!(ax, n_types, times_multi_s; label = "@multi :opt_speed") -scatterlines!(ax, n_types, times_multi_m; label = "@multi :opt_memory") +scatterlines!(ax, n_types, times_multi_s; label = "@sumtype") ax.xlabel = "# types" ax.ylabel = "time relative to 1 type" -ax.title = "Union types vs @multiagent macro" +ax.title = "Union types vs @sumtype" axislegend(ax; position = :lt) ax.yticks = 0:1:ceil(Int, maximum(times_n)) -ax.xticks = 2:2:16 +ax.xticks = [2, 3, 4, 5, 10, 15] fig From c034393f8ff8487e27c6749836376e95f1d46f4b Mon Sep 17 00:00:00 2001 From: Tortar Date: Sun, 7 Jul 2024 18:38:00 +0200 Subject: [PATCH 04/54] more --- docs/src/performance_tips.md | 11 ++++ examples/event_rock_paper_scissors.jl | 5 +- examples/rabbit_fox_hawk.jl | 59 +++++++++++-------- src/core/model_event_queue.jl | 4 +- ...n_dispatch.jl => sum_faster_than_multi.jl} | 14 +++-- 5 files changed, 60 insertions(+), 33 deletions(-) rename test/performance/{branching_faster_than_dispatch.jl => sum_faster_than_multi.jl} (93%) diff --git a/docs/src/performance_tips.md b/docs/src/performance_tips.md index 820f08f255..8d48c88bbd 100644 --- a/docs/src/performance_tips.md +++ b/docs/src/performance_tips.md @@ -138,3 +138,14 @@ include(t) We see that Unions of up to three different Agent types do not suffer much. Hence, if you have less than four agent types in your model, using different types is still a valid option. For more agent types however we recommend using the [`@sumtype`](@ref) macro. + +Finally, we also have a more realist benchmark of the two approaches at `test/performance/sum_faster_than_multi.jl` where the +result of running the model with the two methodologies are + +```@example performance_2 +using Agents +x = pathof(Agents) +t = joinpath(dirname(dirname(x)), "test", "performance", "sum_faster_than_multi.jl") +include(t) +``` + diff --git a/examples/event_rock_paper_scissors.jl b/examples/event_rock_paper_scissors.jl index 66c9879418..fcbf1a7f51 100644 --- a/examples/event_rock_paper_scissors.jl +++ b/examples/event_rock_paper_scissors.jl @@ -35,7 +35,7 @@ # We start by loading `Agents` -using Agents +using Agents, DynamicSumTypes # and defining the three agent types @@ -155,7 +155,7 @@ end movement_event = AgentEvent( action! = move!, propensity = movement_propensity, - types = (Scissors, Paper), timing = movement_time + types = Union{Scissors, Paper}, timing = movement_time ) # we wrap all events in a tuple and we are done with the setting up part! @@ -174,6 +174,7 @@ space = GridSpaceSingle((100, 100)) using Random: Xoshiro rng = Xoshiro(42) +const RPS = Union{Rock, Paper, Scissors} model = EventQueueABM(RPS, events, space; rng, warn = false) # populating the model with agents is the same as in the main [Tutorial](@ref), diff --git a/examples/rabbit_fox_hawk.jl b/examples/rabbit_fox_hawk.jl index bb05f13a81..b6272448af 100644 --- a/examples/rabbit_fox_hawk.jl +++ b/examples/rabbit_fox_hawk.jl @@ -21,21 +21,24 @@ # and reproduce. Eating food (grass or rabbits) replenishes `energy` by a fixed amount. using Agents, Agents.Pathfinding using Random +using DynamicSumTypes import ImageMagick using FileIO: load -@multiagent :opt_speed struct Animal(ContinuousAgent{3,Float64}) - @subagent struct Rabbit - energy::Float64 - end - @subagent struct Fox - energy::Float64 - end - @subagent struct Hawk - energy::Float64 - end +@agent struct Rabbit(ContinuousAgent{3,Float64}) + energy::Float64 +end + +@agent struct Fox(ContinuousAgent{3,Float64}) + energy::Float64 end +@agent struct Hawk(ContinuousAgent{3,Float64}) + energy::Float64 +end + +@sumtype Animal(Rabbit, Fox, Hawk) <: AbstractAgent + # A utility function to find the euclidean norm of a Vector eunorm(vec) = √sum(vec .^ 2) const v0 = (0.0, 0.0, 0.0) # we don't use the velocity field here @@ -147,15 +150,18 @@ function initialize_model( ## spawn each animal at a random walkable position according to its pathfinder for _ in 1:n_rabbits pos = random_walkable(model, model.landfinder) - add_agent!(pos, Rabbit, model, v0, rand(abmrng(model), Δe_grass:2Δe_grass)) + agent = Animal(Rabbit(model, random_position(model), v0, rand(abmrng(model), Δe_grass:2Δe_grass))) + add_agent_own_pos!(agent, model) end for _ in 1:n_foxes pos = random_walkable(model, model.landfinder) - add_agent!(pos, Fox, model, v0, rand(abmrng(model), Δe_rabbit:2Δe_rabbit)) + agent = Animal(Fox(model, random_position(model), v0, rand(abmrng(model), Δe_rabbit:2Δe_rabbit))) + add_agent_own_pos!(agent, model) end for _ in 1:n_hawks pos = random_walkable(model, model.airfinder) - add_agent!(pos, Hawk, model, v0, rand(abmrng(model), Δe_rabbit:2Δe_rabbit)) + agent = Animal(Hawk(model, random_position(model), v0, rand(abmrng(model), Δe_rabbit:2Δe_rabbit))) + add_agent_own_pos!(agent, model) end return model end @@ -173,7 +179,9 @@ end # ones contributing more to the chosen direction. If there are no predators to flee from, # rabbits walk around randomly. -@dispatch function animal_step!(rabbit::Rabbit, model) +animal_step!(animal, model) = animal_step!(animal, model, variant(animal)) + +function animal_step!(rabbit, model, ::Rabbit) ## Eat grass at this position, if any if get_spatial_property(rabbit.pos, model.grass, model) == 1 model.grass[get_spatial_index(rabbit.pos, model.grass, model)] = 0 @@ -192,7 +200,7 @@ end ## Get a list of positions of all nearby predators predators = [ x.pos for x in nearby_agents(rabbit, model, model.rabbit_vision) if - kindof(x) == :fox || kindof(x) == :hawk + variant(x) isa Fox || variant(x) isa Hawk ] ## If the rabbit sees a predator and isn't already moving somewhere if !isempty(predators) && is_stationary(rabbit, model.landfinder) @@ -241,9 +249,9 @@ end # Foxes hunt for rabbits, and eat rabbits within a unit radius of its position. -@dispatch function animal_step!(fox::Fox, model) +function animal_step!(fox, model, ::Fox) ## Look for nearby rabbits that can be eaten - food = [x for x in nearby_agents(fox, model) if kindof(x) == :rabbit] + food = [x for x in nearby_agents(fox, model) if variant(x) isa Rabbit] if !isempty(food) remove_agent!(rand(abmrng(model), food), model, model.landfinder) fox.energy += model.Δe_rabbit @@ -265,7 +273,7 @@ end ## If the fox isn't already moving somewhere if is_stationary(fox, model.landfinder) ## Look for any nearby rabbits - prey = [x for x in nearby_agents(fox, model, model.fox_vision) if kindof(x) == :rabbit] + prey = [x for x in nearby_agents(fox, model, model.fox_vision) if variant(x) isa Rabbit] if isempty(prey) ## Move anywhere if no rabbits were found plan_route!( @@ -285,9 +293,9 @@ end # Hawks function similarly to foxes, except they can also fly. They dive down for prey and # fly back up after eating it. -@dispatch function animal_step!(hawk::Hawk, model) +function animal_step!(hawk, model, ::Hawk) ## Look for rabbits nearby - food = [x for x in nearby_agents(hawk, model) if kindof(x) == :rabbit] + food = [x for x in nearby_agents(hawk, model) if variant(x) isa Rabbit] if !isempty(food) ## Eat (remove) the rabbit remove_agent!(rand(abmrng(model), food), model, model.airfinder) @@ -307,7 +315,7 @@ end rand(abmrng(model)) <= model.hawk_repr * model.dt && reproduce!(hawk, model) if is_stationary(hawk, model.airfinder) - prey = [x for x in nearby_agents(hawk, model, model.hawk_vision) if kindof(x) == :rabbit] + prey = [x for x in nearby_agents(hawk, model, model.hawk_vision) if variant(x) isa Rabbit] if isempty(prey) plan_route!( hawk, @@ -327,7 +335,8 @@ end function reproduce!(animal, model) animal.energy = Float64(ceil(Int, animal.energy / 2)) - add_agent!(animal.pos, eval(kindof(animal)), model, v0, animal.energy) + new_agent = Animal(typeof(variant(animal))(model, random_position(model), v0, animal.energy)) + add_agent!(new_agent, model) end # The model stepping function simulates the growth of grass @@ -359,9 +368,9 @@ model = initialize_model() # using GLMakie # CairoMakie doesn't do 3D plots well # ``` -@dispatch animalcolor(a::Rabbit) = :brown -@dispatch animalcolor(a::Fox) = :orange -@dispatch animalcolor(a::Hawk) = :blue +animalcolor(a::Rabbit) = :brown +animalcolor(a::Fox) = :orange +animalcolor(a::Hawk) = :blue # We use `surface!` to plot the terrain as a mesh, and colour it using the `:terrain` # colormap. Since the heightmap dimensions don't correspond to the dimensions of the space, diff --git a/src/core/model_event_queue.jl b/src/core/model_event_queue.jl index bfeec7bfa3..e7dfb477f7 100644 --- a/src/core/model_event_queue.jl +++ b/src/core/model_event_queue.jl @@ -164,9 +164,9 @@ function EventQueueABM( agent_types = union_types(A) type_to_idx = Dict(t => i for (i, t) in enumerate(agent_types)) - # precompute a vector mapping the agent kind index to a + # precompute a vector mapping the agent type index to a # vectors of indices, each vector corresponding - # to all valid events that can apply to a given agent kind + # to all valid events that can apply to a given agent type idx_events_each_type = [[i for (i, e) in enumerate(events) if t <: e.types] for t in agent_types] # initialize vectors for the propensities (they are updated in-place later) diff --git a/test/performance/branching_faster_than_dispatch.jl b/test/performance/sum_faster_than_multi.jl similarity index 93% rename from test/performance/branching_faster_than_dispatch.jl rename to test/performance/sum_faster_than_multi.jl index a4bde8f87a..10b3d7054a 100644 --- a/test/performance/branching_faster_than_dispatch.jl +++ b/test/performance/sum_faster_than_multi.jl @@ -148,16 +148,22 @@ end ################### Benchmarks ############### -@btime step!($model1, 50) -@btime step!($model2, 50) +t1 = @belapsed step!($model1, 50) +t2 = @belapsed step!($model2, 50) # Results: # multiple types: 3.778 s (38740900 allocations: 2.38 GiB) # @sumtype: 545.119 ms (22952850 allocations: 965.93 MiB) -Base.summarysize(model1) -Base.summarysize(model2) +m1 = Base.summarysize(model1) +m2 = Base.summarysize(model2) # Results: # multiple types: 543.496 KiB # @sumtype: 546.360 KiB + +println("Performance of the model with multiple types: $(t1) s") +println("Performance of the model with @sumtype: $(t2) s") +println() +println("Memory occupied by the model with multiple types: $(m1/1000) Kib") +println("Memory occupied by the model with @sumtype: $(m2/1000) Kib") From d056d2ad863efe580c9eefef2290f4e061ea1df2 Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Sun, 7 Jul 2024 18:51:35 +0200 Subject: [PATCH 05/54] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 8c7d09e225..4728c0db2d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Agents" uuid = "46ada45e-f475-11e8-01d0-f70cc89e6671" authors = ["George Datseris", "Tim DuBois", "Aayush Sabharwal", "Ali Vahdati", "Adriano Meligrana"] -version = "6.0.16" +version = "6.1.0" [deps] CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" From aa0301ad9dfc7d82cb29a1d5d3528cbd9693dded Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Sun, 7 Jul 2024 19:19:11 +0200 Subject: [PATCH 06/54] Update tutorial.md --- docs/src/tutorial.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index bebe47bc28..34ff9beab0 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -736,7 +736,7 @@ and, when making the model we would specify ````@example tutorial model = StandardABM( - Union{SchellingAgent, Politician}, # type of agents + Union{Schelling, Politician}, # type of agents space; # space they live in ) ```` @@ -745,7 +745,7 @@ Naturally, we would have to define a new agent stepping function that would act differently depending on the agent type ````@example tutorial -function agent_step!(agent::SchellingAgent, model) +function agent_step!(agent::Schelling, model) # stuff. end @@ -758,7 +758,7 @@ and then passing ````@example tutorial model = StandardABM( - Union{SchellingAgent, Politician}, # type of agents + Union{Schelling, Politician}, # type of agents space; # space they live in agent_step! = union_step! ) @@ -773,7 +773,7 @@ everything works the same ````@example tutorial using DynamicSumTypes -@sumtype MultiSchelling(SchellingAgent, Politician) <: AbstractAgent +@sumtype MultiSchelling(Schelling, Politician) <: AbstractAgent ```` Now when you create instances you will need to enclose them in `MultiSchelling` @@ -799,7 +799,7 @@ instead. Hence, the agent stepping function should become something like ````@example tutorial agent_step!(agent, model) = agent_step!(agent, model, variant(agent)) -function agent_step!(agent, model, ::SchellingAgent) +function agent_step!(agent, model, ::Schelling) # stuff. end @@ -829,13 +829,13 @@ agent. For example, in the union case we provide the `Union` type when we create the model, ````@example tutorial -model = StandardABM(Union{SchellingAgent, Politician}, space) +model = StandardABM(Union{Schelling, Politician}, space) ```` we add them by specifying the type ````@example tutorial -add_agent_single!(SchellingAgent, model; group = 1, mood = true) +add_agent_single!(Schelling, model; group = 1, mood = true) ```` or @@ -859,7 +859,7 @@ model = StandardABM(MultiSchelling, space) we add agents like so ````@example tutorial -agent = MultiSchelling(SchellingAgent(model; pos = random_position(model), group = 1, mood = true)) +agent = MultiSchelling(Schelling(model; pos = random_position(model), group = 1, mood = true)) add_agent_single!(agent, model) ```` From 73dd69747dbaaa7081f066dc1c93f7af51160347 Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Sun, 7 Jul 2024 19:19:59 +0200 Subject: [PATCH 07/54] Update tutorial.jl --- docs/src/tutorial.jl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/src/tutorial.jl b/docs/src/tutorial.jl index 928310d1ed..e54b2c1ef1 100644 --- a/docs/src/tutorial.jl +++ b/docs/src/tutorial.jl @@ -707,7 +707,7 @@ end # and, when making the model we would specify model = StandardABM( - Union{SchellingAgent, Politician}, # type of agents + Union{Schelling, Politician}, # type of agents space; # space they live in ) @@ -715,7 +715,7 @@ model = StandardABM( # act differently depending on the agent type. This could be done by making # a function that calls other functions depending on the type, such as -function agent_step!(agent::SchellingAgent, model) +function agent_step!(agent::Schelling, model) # stuff. end @@ -726,7 +726,7 @@ end # and then passing model = StandardABM( - Union{SchellingAgent, Politician}, # type of agents + Union{Schelling, Politician}, # type of agents space; # space they live in agent_step! = union_step! ) @@ -739,7 +739,7 @@ model = StandardABM( using DynamicSumTypes -@sumtype MultiSchelling(SchellingAgent, Politician) <: AbstractAgent +@sumtype MultiSchelling(Schelling, Politician) <: AbstractAgent # Now when you create instances you will need to enclose them in `MultiSchelling` @@ -757,7 +757,7 @@ typeof(variant(gov)) agent_step!(agent, model) = agent_step!(agent, model, variant(agent)) -function agent_step!(agent, model, ::SchellingAgent) +function agent_step!(agent, model, ::Schelling) # stuff. end @@ -783,11 +783,11 @@ model = StandardABM( # For example, in the union case we provide the `Union` type when we create the model, -model = StandardABM(Union{SchellingAgent, Politician}, space) +model = StandardABM(Union{Schelling, Politician}, space) # we add them by specifying the type -add_agent_single!(SchellingAgent, model; group = 1, mood = true) +add_agent_single!(Schelling, model; group = 1, mood = true) # or @@ -803,7 +803,7 @@ model = StandardABM(MultiSchelling, space) # we add agents like so -agent = MultiSchelling(SchellingAgent(model; pos = random_position(model), group = 1, mood = true)) +agent = MultiSchelling(Schelling(model; pos = random_position(model), group = 1, mood = true)) add_agent_single!(agent, model) # or From 563a04e25ad6597b8f73643d9907cf3f46179277 Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Sun, 7 Jul 2024 19:21:23 +0200 Subject: [PATCH 08/54] Update tutorial.jl --- docs/src/tutorial.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/tutorial.jl b/docs/src/tutorial.jl index e54b2c1ef1..4e41186b41 100644 --- a/docs/src/tutorial.jl +++ b/docs/src/tutorial.jl @@ -728,7 +728,7 @@ end model = StandardABM( Union{Schelling, Politician}, # type of agents space; # space they live in - agent_step! = union_step! + agent_step! ) # ## Multiple agent types with `@sumtype` @@ -770,7 +770,7 @@ end model = StandardABM( MultiSchelling, # the sum type is given as the type space; - agent_step! = multi_step! + agent_step! ) # ## Adding agents of different types to the model From edf06e3d24b7f7b5a06cb6b3efbe923b4ebe5759 Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Sun, 7 Jul 2024 19:21:49 +0200 Subject: [PATCH 09/54] Update tutorial.md --- docs/src/tutorial.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index 34ff9beab0..cf511fbea6 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -760,7 +760,7 @@ and then passing model = StandardABM( Union{Schelling, Politician}, # type of agents space; # space they live in - agent_step! = union_step! + agent_step! ) ```` @@ -814,7 +814,7 @@ and you need to give `MultiSchelling` as the type of agents in model initializat model = StandardABM( MultiSchelling, # the sum type is given as the type space; - agent_step! = multi_step! + agent_step! ) ```` From 60c8964a9fc34d847134779e01f707d0476103e4 Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Sun, 7 Jul 2024 19:23:38 +0200 Subject: [PATCH 10/54] Update tutorial.jl --- docs/src/tutorial.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/tutorial.jl b/docs/src/tutorial.jl index 4e41186b41..8a59c0e5b9 100644 --- a/docs/src/tutorial.jl +++ b/docs/src/tutorial.jl @@ -743,15 +743,15 @@ using DynamicSumTypes # Now when you create instances you will need to enclose them in `MultiSchelling` -gov = MultiSchelling(Governor(; id = 3 , pos = (2, 2), group = 2, influence = 0.5)) +p = MultiSchelling(Politician(model; pos = random_position(model), preferred_demographic = 1)) # agents are then all of type `MultiSchelling` -typeof(gov) +typeof(p) # and hence you can't use only `typeof` to differentiate them. But you can use -typeof(variant(gov)) +typeof(variant(p)) # instead. Hence, the agent stepping function should become something like From fe6f8f739e4770d95b056ced0a6ed1df75db0fac Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Sun, 7 Jul 2024 19:24:19 +0200 Subject: [PATCH 11/54] Update tutorial.md --- docs/src/tutorial.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index cf511fbea6..c509e8d46d 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -779,19 +779,19 @@ using DynamicSumTypes Now when you create instances you will need to enclose them in `MultiSchelling` ````@example tutorial -gov = MultiSchelling(Governor(; id = 3 , pos = (2, 2), group = 2, influence = 0.5)) +p = MultiSchelling(Politician(model; pos = random_position(model), preferred_demographic = 1)) ```` agents are then all of type `MultiSchelling` ````@example tutorial -typeof(gov) +typeof(p) ```` and hence you can't use only `typeof` to differentiate them. But you can use ````@example tutorial -typeof(variant(gov)) +typeof(variant(p)) ```` instead. Hence, the agent stepping function should become something like From 9a2417c150fa010bea99359babb5a5b5681b1245 Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Sun, 7 Jul 2024 23:36:18 +0200 Subject: [PATCH 12/54] Update tutorial.md --- docs/src/tutorial.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index c509e8d46d..891ee47bd1 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -746,11 +746,11 @@ act differently depending on the agent type ````@example tutorial function agent_step!(agent::Schelling, model) - # stuff. + "some stuff." end function agent_step!(agent::Politician, model) - # other stuff. + "other stuff." end ```` @@ -800,11 +800,11 @@ instead. Hence, the agent stepping function should become something like agent_step!(agent, model) = agent_step!(agent, model, variant(agent)) function agent_step!(agent, model, ::Schelling) - # stuff. + "some stuff." end function agent_step!(agent, model, ::Politician) - # other stuff. + "other stuff." end ```` From a1a1bda84db33a70e900be3d4ee31d163fb5e96e Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Mon, 8 Jul 2024 00:13:03 +0200 Subject: [PATCH 13/54] Update tutorial.md --- docs/src/tutorial.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index 891ee47bd1..01e7bbec1d 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -746,11 +746,11 @@ act differently depending on the agent type ````@example tutorial function agent_step!(agent::Schelling, model) - "some stuff." + # some stuff. end function agent_step!(agent::Politician, model) - "other stuff." + # other stuff. end ```` @@ -800,11 +800,11 @@ instead. Hence, the agent stepping function should become something like agent_step!(agent, model) = agent_step!(agent, model, variant(agent)) function agent_step!(agent, model, ::Schelling) - "some stuff." + # some stuff. end function agent_step!(agent, model, ::Politician) - "other stuff." + # other stuff. end ```` From ac842b94c76c99db3ebcead231e8f53a7bf80ce1 Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Mon, 8 Jul 2024 00:13:41 +0200 Subject: [PATCH 14/54] Update tutorial.jl --- docs/src/tutorial.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/tutorial.jl b/docs/src/tutorial.jl index 8a59c0e5b9..e029c5ba5b 100644 --- a/docs/src/tutorial.jl +++ b/docs/src/tutorial.jl @@ -716,11 +716,11 @@ model = StandardABM( # a function that calls other functions depending on the type, such as function agent_step!(agent::Schelling, model) - # stuff. + ## stuff. end function agent_step!(agent::Politician, model) - # other stuff. + ## other stuff. end # and then passing @@ -758,11 +758,11 @@ typeof(variant(p)) agent_step!(agent, model) = agent_step!(agent, model, variant(agent)) function agent_step!(agent, model, ::Schelling) - # stuff. + ## stuff. end function agent_step!(agent, model, ::Politician) - # other stuff. + ## other stuff. end # and you need to give `MultiSchelling` as the type of agents in model initialization From a31bcc119ef62816e6f2f93365ced5e2a268a85a Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Mon, 8 Jul 2024 01:47:27 +0200 Subject: [PATCH 15/54] Update performance_tips.md --- docs/src/performance_tips.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/src/performance_tips.md b/docs/src/performance_tips.md index 8d48c88bbd..b83b22065d 100644 --- a/docs/src/performance_tips.md +++ b/docs/src/performance_tips.md @@ -120,7 +120,6 @@ For an example of how this is done, see the [Forest fire](@ref) model, which is ## [Multiple agent types: `@sumtype` versus `Union` types](@id sum_vs_union) Due to the way Julia's type system works, and the fact that agents are grouped in a container mapping IDs to agent instances, using a `Union` for different agent types always creates a performance hit because it leads to type instability. -On the other hand, a `Union` of different types allows utilizing Julia's multiple dispatch system. The [`@sumtype`](@ref) macro enclose all types in a single one making working with it type stable. From df599b47d7dffacf243185846ab1beccdb5a4ece Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Mon, 8 Jul 2024 02:56:10 +0200 Subject: [PATCH 16/54] Update performance_tips.md --- docs/src/performance_tips.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/performance_tips.md b/docs/src/performance_tips.md index b83b22065d..25aafd585e 100644 --- a/docs/src/performance_tips.md +++ b/docs/src/performance_tips.md @@ -138,7 +138,7 @@ We see that Unions of up to three different Agent types do not suffer much. Hence, if you have less than four agent types in your model, using different types is still a valid option. For more agent types however we recommend using the [`@sumtype`](@ref) macro. -Finally, we also have a more realist benchmark of the two approaches at `test/performance/sum_faster_than_multi.jl` where the +Finally, we also have a more realistic benchmark of the two approaches at `test/performance/sum_faster_than_multi.jl` where the result of running the model with the two methodologies are ```@example performance_2 From bf4cf03a6ea9f18cf638ca16020d3feea5543d09 Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Mon, 8 Jul 2024 02:57:10 +0200 Subject: [PATCH 17/54] Update sum_faster_than_multi.jl --- test/performance/sum_faster_than_multi.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/performance/sum_faster_than_multi.jl b/test/performance/sum_faster_than_multi.jl index 10b3d7054a..7b55685b3f 100644 --- a/test/performance/sum_faster_than_multi.jl +++ b/test/performance/sum_faster_than_multi.jl @@ -162,8 +162,7 @@ m2 = Base.summarysize(model2) # multiple types: 543.496 KiB # @sumtype: 546.360 KiB -println("Performance of the model with multiple types: $(t1) s") -println("Performance of the model with @sumtype: $(t2) s") -println() +println("Time to step the model with multiple types: $(t1) s") +println("Time to step the model with @sumtype: $(t2) s") println("Memory occupied by the model with multiple types: $(m1/1000) Kib") println("Memory occupied by the model with @sumtype: $(m2/1000) Kib") From b935641bdff37a6165feeb94185c11cdba911899 Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 9 Jul 2024 23:40:39 +0200 Subject: [PATCH 18/54] Update sum_faster_than_multi.jl --- test/performance/sum_faster_than_multi.jl | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/performance/sum_faster_than_multi.jl b/test/performance/sum_faster_than_multi.jl index 7b55685b3f..593ffdb7e6 100644 --- a/test/performance/sum_faster_than_multi.jl +++ b/test/performance/sum_faster_than_multi.jl @@ -60,13 +60,15 @@ function agent_step!(agent::GridAgentFour, model1) agent.one += sum(a.one for a in nearby_agents(agent, model1)) end function agent_step!(agent::GridAgentFive, model1) - targets = filter!(a->a.one > 0.8, collect(nearby_agents(agent, model1, 3))) - idx = argmax(map(t->euclidean_distance(agent, t, model1), targets)) - farthest = targets[idx] - walk!(agent, sign.(farthest.pos .- agent.pos), model1) + targets = filter!(a->a.one > 1.0, collect(nearby_agents(agent, model1, 3))) + if !isempty(targets) + idx = argmax(map(t->euclidean_distance(agent, t, model1), targets)) + farthest = targets[idx] + walk!(agent, sign.(farthest.pos .- agent.pos), model1) + end end function agent_step!(agent::GridAgentSix, model1) - agent.eight += sum(rand(abmrng(model2), (0, 1)) for a in nearby_agents(agent, model1)) + agent.eight += sum(rand(abmrng(model1), (0, 1)) for a in nearby_agents(agent, model1)) end model1 = StandardABM( From 1ecd0732f5324862c28423b820a42a0310a8f8b9 Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 9 Jul 2024 23:46:37 +0200 Subject: [PATCH 19/54] Update sum_faster_than_multi.jl --- test/performance/sum_faster_than_multi.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/performance/sum_faster_than_multi.jl b/test/performance/sum_faster_than_multi.jl index 593ffdb7e6..2c8a26df26 100644 --- a/test/performance/sum_faster_than_multi.jl +++ b/test/performance/sum_faster_than_multi.jl @@ -154,7 +154,7 @@ t1 = @belapsed step!($model1, 50) t2 = @belapsed step!($model2, 50) # Results: -# multiple types: 3.778 s (38740900 allocations: 2.38 GiB) +# multiple types: 1.849 s (34267900 allocations: 1.96 GiB) # @sumtype: 545.119 ms (22952850 allocations: 965.93 MiB) m1 = Base.summarysize(model1) From b3399f7db7600a07f19ef220f1445420ebe8ca35 Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:17:23 +0200 Subject: [PATCH 20/54] Update sum_faster_than_multi.jl --- test/performance/sum_faster_than_multi.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/performance/sum_faster_than_multi.jl b/test/performance/sum_faster_than_multi.jl index 2c8a26df26..2f3588b4e8 100644 --- a/test/performance/sum_faster_than_multi.jl +++ b/test/performance/sum_faster_than_multi.jl @@ -45,6 +45,8 @@ end eight::Int64 end +const types = Union{GridAgentOne, GridAgentTwo, GridAgentThree, GridAgentFour, GridAgentFive, GridAgentSix} + agent_step!(agent::GridAgentOne, model1) = randomwalk!(agent, model1) function agent_step!(agent::GridAgentTwo, model1) agent.one += rand(abmrng(model1)) @@ -60,7 +62,7 @@ function agent_step!(agent::GridAgentFour, model1) agent.one += sum(a.one for a in nearby_agents(agent, model1)) end function agent_step!(agent::GridAgentFive, model1) - targets = filter!(a->a.one > 1.0, collect(nearby_agents(agent, model1, 3))) + targets = filter!(a->a.one > 1.0, collect(types, nearby_agents(agent, model1, 3))) if !isempty(targets) idx = argmax(map(t->euclidean_distance(agent, t, model1), targets)) farthest = targets[idx] @@ -110,7 +112,7 @@ function agent_step!(agent, model2, ::GridAgentFour) agent.one += sum(a.one for a in nearby_agents(agent, model2)) end function agent_step!(agent, model2, ::GridAgentFive) - targets = filter!(a->a.one > 1.0, collect(nearby_agents(agent, model2, 3))) + targets = filter!(a->a.one > 1.0, collect(GridAgentAll, nearby_agents(agent, model2, 3))) if !isempty(targets) idx = argmax(map(t->euclidean_distance(agent, t, model2), targets)) farthest = targets[idx] From 61198a17935d730a0b95821adbc27ad421e3e7bd Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:17:47 +0200 Subject: [PATCH 21/54] Update Project.toml --- docs/Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Project.toml b/docs/Project.toml index 0758cd1e6e..544bf91d5d 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -34,6 +34,7 @@ StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" [compat] +julia = "1.11" BlackBoxOptim = "0.6" CellListMap = "0.9" Documenter = "1" From 7fb6e693ac08ee2d6d2caddc4c9122f7e83e1dff Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Fri, 12 Jul 2024 01:20:29 +0200 Subject: [PATCH 22/54] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81b4cac140..ad42d5965f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: fail-fast: false matrix: version: - - '1' + - '1.11' os: - ubuntu-20.04 arch: From 373a148899c4880d825c13a47f476c8091aa66cc Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Fri, 12 Jul 2024 02:04:02 +0200 Subject: [PATCH 23/54] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad42d5965f..b76006e815 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: version: - - '1' + - '1.11' os: [ubuntu-latest, windows-latest, macos-latest] arch: - x64 From df010929a460145853368eab0ba74de5793f6c8f Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Fri, 12 Jul 2024 02:04:18 +0200 Subject: [PATCH 24/54] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b76006e815..a79fc35fcd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,7 +71,7 @@ jobs: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v1 with: - version: '1' + version: '1.11' - uses: actions/cache@v4 env: cache-name: cache-artifacts From b9c67e5a62cc667a4c2175475c7ffde90940f81c Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Fri, 12 Jul 2024 02:18:29 +0200 Subject: [PATCH 25/54] Update ci.yml --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a79fc35fcd..17ed9f40c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: version: - - '1.11' + - 'nightly' os: [ubuntu-latest, windows-latest, macos-latest] arch: - x64 @@ -56,7 +56,7 @@ jobs: fail-fast: false matrix: version: - - '1.11' + - 'nightly' os: - ubuntu-20.04 arch: @@ -71,7 +71,7 @@ jobs: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v1 with: - version: '1.11' + version: 'nightly' - uses: actions/cache@v4 env: cache-name: cache-artifacts From 324952c78de6a344f406205f99791e5dccaabb14 Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Thu, 18 Jul 2024 22:48:47 +0200 Subject: [PATCH 26/54] Update ci.yml --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17ed9f40c5..81b4cac140 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: version: - - 'nightly' + - '1' os: [ubuntu-latest, windows-latest, macos-latest] arch: - x64 @@ -56,7 +56,7 @@ jobs: fail-fast: false matrix: version: - - 'nightly' + - '1' os: - ubuntu-20.04 arch: @@ -71,7 +71,7 @@ jobs: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v1 with: - version: 'nightly' + version: '1' - uses: actions/cache@v4 env: cache-name: cache-artifacts From 196078fe59c90de9d20ea6111869cb8050bb2d32 Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:21:34 +0200 Subject: [PATCH 27/54] Update agents.jl --- src/core/agents.jl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/core/agents.jl b/src/core/agents.jl index 31dc2e006d..699ee748d7 100644 --- a/src/core/agents.jl +++ b/src/core/agents.jl @@ -252,3 +252,15 @@ function compute_base_fields(base_type_spec) @capture(base_agent, mutable struct _ <: _ base_fields__ end) return base_fields end + +macro multiagent(typedef) + if typedef.head == :struct + @warn "This version of @multiagent is deprecated because the underlying package + implementing the backend for it was updated to a much simpler methodology, + refer to the updated Tutorial in the documentation to update your + model to use the new methodology." + return esc(_multiagent(QuoteNode(:opt_speed), struct_repr)) + else + return esc(:($DynamicSumTypes.@sumtype $typedef)) + end +end From e1e36ba423b9e10385664a1abb1c7accf67b4b79 Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:23:26 +0200 Subject: [PATCH 28/54] Update deprecations.jl --- src/deprecations.jl | 76 ++------------------------------------------- 1 file changed, 2 insertions(+), 74 deletions(-) diff --git a/src/deprecations.jl b/src/deprecations.jl index 2c2e3d0b45..b27c235ef7 100644 --- a/src/deprecations.jl +++ b/src/deprecations.jl @@ -392,87 +392,15 @@ function nearby_agents_exact(a, model, r=1) return (model[id] for id in nearby_ids(a, model, r; search=:exact)) end -export @multiagent, @dispatch, @finalize_dispatch, kindof, allkinds +export @dispatch, @finalize_dispatch, kindof, allkinds export DynamicSumTypes using DynamicSumTypes: allkinds -""" - @multiagent struct YourAgentType{X,Y}(AgentTypeToInherit) [<: OptionalSupertype] - @subagent FirstAgentSubType{X} - first_property::X # shared with second agent - second_property_with_default::Bool = true - end - @subagent SecondAgentSubType{X,Y} - first_property::X = 3 - third_property::Y - end - # etc... - end -Define multiple agent "subtypes", which are actually only variants of a unique -overarching type `YourAgentType`. This means that all "subtypes" are conceptual: they are simply -convenience functions defined that initialize the common proper type correctly -(see examples below for more). Because the "subtypes" are not real Julia `Types`, -you cannot use multiple dispatch on them. You also cannot distinguish them -on the basis of `typeof`, but need to use instead the [`kindof`](@ref) function. -That is why these "types" are often referred to as "kinds" in the documentation. -See also the [`allkinds`](@ref) function for a convenient way to obtain all kinds. -See the [Tutorial](@ref) or the [performance comparison versus `Union` types](@ref sum_vs_union) -for why in most cases it is better to use `@multiagent` than making multiple -agent types manually. See [`@dispatch`](@ref) (also highlighted in the [Tutorial](@ref)) -for a multiple-dispatch-like syntax to use with `@multiagent`. -Two different versions of `@multiagent` can be used by passing either `:opt_speed` or -`:opt_memory` as the first argument (before the `struct` keyword). -The first optimizes the agents representation for -speed, the second does the same for memory, at the cost of a moderate drop in performance. -By default it uses `:opt_speed`. -## Examples -Let's say you have this definition: -``` -@multiagent :opt_speed struct Animal{T}(GridAgent{2}) - @subagent struct Wolf - energy::Float64 = 0.5 - ground_speed::Float64 - const fur_color::Symbol - end - @subagent struct Hawk{T} - energy::Float64 = 0.1 - ground_speed::Float64 - flight_speed::T - end -end -``` -Then you can create `Wolf` and `Hawk` agents normally, like so -``` -hawk_1 = Hawk(1, (1, 1), 1.0, 2.0, 3) -hawk_2 = Hawk(; id = 2, pos = (1, 2), ground_speed = 2.3, flight_speed = 2) -wolf_1 = Wolf(3, (2, 2), 2.0, 3.0, :black) -wolf_2 = Wolf(; id = 4, pos = (2, 1), ground_speed = 2.0, fur_color = :white) -``` -It is important to notice, though, that the `Wolf` and `Hawk` types are just -conceptual and all agents are actually of type `Animal` in this case. -The way to retrieve the variant of the agent is through the function `kindof` e.g. -``` -kindof(hawk_1) # :Hawk -kindof(wolf_2) # :Wolf -``` -See the [rabbit_fox_hawk](@ref) example to see how to use this macro in a model. -## Current limitations -- Impossibility to inherit from a compactified agent. -""" macro multiagent(version, struct_repr) expr = _multiagent(version, struct_repr) return esc(expr) end -macro multiagent(struct_repr) - @warn "@multiagent is deprecated because the underlying package - implementing the backend for it was updated to a much simpler methodology, - refer to the updated Tutorial in the documentation to update your - model to use this new methodology." - expr = _multiagent(QuoteNode(:opt_speed), struct_repr) - return esc(expr) -end - function _multiagent(version, struct_repr) new_type, base_type_spec, abstract_type, agent_specs = decompose_struct_base(struct_repr) base_fields = compute_base_fields(base_type_spec) @@ -595,4 +523,4 @@ end """ macro finalize_dispatch() return esc(:(DynamicSumTypes.@finalize_patterns)) -end \ No newline at end of file +end From 58617e158e73b54ca2305bf5acb1771a3270905a Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:24:02 +0200 Subject: [PATCH 29/54] Update agents.jl --- src/core/agents.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/agents.jl b/src/core/agents.jl index 699ee748d7..d81230546e 100644 --- a/src/core/agents.jl +++ b/src/core/agents.jl @@ -1,4 +1,4 @@ -export AbstractAgent, @agent, NoSpaceAgent +export AbstractAgent, @agent, @multiagent, NoSpaceAgent ########################################################################################### # @agent From 569599a059874558a677e07e68607920e0388ddc Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:27:08 +0200 Subject: [PATCH 30/54] Update deprecations.jl --- src/deprecations.jl | 39 --------------------------------------- 1 file changed, 39 deletions(-) diff --git a/src/deprecations.jl b/src/deprecations.jl index b27c235ef7..dfd7cdba86 100644 --- a/src/deprecations.jl +++ b/src/deprecations.jl @@ -466,61 +466,22 @@ function _multiagent(version, struct_repr) return expr end -# This function is extended in the `@multiagent` macro ismultiagenttype(::Type) = false ismultiagentsumtype(::Type) = false ismultiagentcompacttype(::Type) = false -""" - kindof(agent::AbstractAgent) → kind::Symbol -Return the "kind" (instead of type) of the agent, which is the name given to the -agent subtype when it was created with [`@multiagent`](@ref). -""" function DynamicSumTypes.kindof(a::AbstractAgent) throw(ArgumentError("Agent of type $(typeof(a)) has not been created via `@multiagent`.")) end -""" - allkinds(AgentType::Type) → kinds::Tuple -Return all "kinds" that compose the given agent type that was generated via -the [`@multiagent`](@ref) macro. The kinds are returned as a tuple of `Symbol`s. -""" function DynamicSumTypes.allkinds(a::Type{<:AbstractAgent}) (nameof(a), ) # this function is extended automatically in the macro end -""" - @dispatch f(args...) -A macro to enable multiple-dispatch-like behavior for the function `f`, -for various agent kinds generated via the [`@multiagent`](@ref) macro. -For an illustration of its usage see the [Tutorial](@ref). -If you are creating your own module/package that uses Agents.jl, and you -are using `@dispatch` inside it, then you need to put [`@finalize_dispatch`](@ref)`()` -before the module end (but after all `@dispatch` calls). -""" macro dispatch(f_def) return esc(:(DynamicSumTypes.@pattern $f_def)) end -""" - @finalize_dispatch -A macro to finalize the definitions of the methods generated by the -`@dispatch` macro when used in a module. It just needs to be used -once at the end of the module if no `@dispatch` method is used inside -of it. Otherwise use it also before the invocations of the methods. -## Examples -```julia -module SomeModel -@multiagent struct MultiAgent(NoSpaceAgent) - @subagent SubAgent1 end - @subagent SubAgent2 end -end -@dispatch MethodSub1(::SubAgent1) = 1 -@dispatch MethodSub1(::SubAgent2) = 1 -@finalize_dispatch() -end -``` -""" macro finalize_dispatch() return esc(:(DynamicSumTypes.@finalize_patterns)) end From 0e9aaea2035ade8ea73d80bbde61cd89455876e5 Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:11:08 +0200 Subject: [PATCH 31/54] Update agents.jl --- src/core/agents.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/agents.jl b/src/core/agents.jl index d81230546e..238d091def 100644 --- a/src/core/agents.jl +++ b/src/core/agents.jl @@ -259,7 +259,7 @@ macro multiagent(typedef) implementing the backend for it was updated to a much simpler methodology, refer to the updated Tutorial in the documentation to update your model to use the new methodology." - return esc(_multiagent(QuoteNode(:opt_speed), struct_repr)) + return esc(_multiagent(QuoteNode(:opt_speed), typedef)) else return esc(:($DynamicSumTypes.@sumtype $typedef)) end From 98a89195f1d951ea036b7a52fb74d1a1c7a22bea Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:16:55 +0200 Subject: [PATCH 32/54] Update performance_tips.md --- docs/src/performance_tips.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/src/performance_tips.md b/docs/src/performance_tips.md index 25aafd585e..ac7c6be22c 100644 --- a/docs/src/performance_tips.md +++ b/docs/src/performance_tips.md @@ -117,11 +117,11 @@ Specifically, you can represent this property as a standard Julia `Array` that i For an example of how this is done, see the [Forest fire](@ref) model, which is a cellular automaton that has no agents in it, or the [Daisyworld](@ref) model, which has both agents as well as a spatial property represented by an `Array`. -## [Multiple agent types: `@sumtype` versus `Union` types](@id sum_vs_union) +## [Multiple agent types: `@multiagent` versus `Union` types](@id sum_vs_union) Due to the way Julia's type system works, and the fact that agents are grouped in a container mapping IDs to agent instances, using a `Union` for different agent types always creates a performance hit because it leads to type instability. -The [`@sumtype`](@ref) macro enclose all types in a single one making working with it type stable. +The [`@multiagent`](@ref) macro enclose all types in a single one making working with it type stable. In the following script, which you can find in `test/performance/variable_agent_types_simple_dynamics.jl`, we create a basic money-exchange ABM with many different agent types (up to 15), while having the simulation rules the same regardless of how many agent types are there. We then compare the performance of the two versions for multiple agent types, incrementally employing more agents from 2 to 15. @@ -136,15 +136,20 @@ include(t) We see that Unions of up to three different Agent types do not suffer much. Hence, if you have less than four agent types in your model, using different types is still a valid option. -For more agent types however we recommend using the [`@sumtype`](@ref) macro. +For more agent types however we recommend using the [`@multiagent`](@ref) macro. -Finally, we also have a more realistic benchmark of the two approaches at `test/performance/sum_faster_than_multi.jl` where the +Finally, we also have a more realistic benchmark of the two approaches at `test/performance/multiagent_vs_union.jl` where the result of running the model with the two methodologies are ```@example performance_2 using Agents x = pathof(Agents) -t = joinpath(dirname(dirname(x)), "test", "performance", "sum_faster_than_multi.jl") +t = joinpath(dirname(dirname(x)), "test", "performance", "multiagent_vs_union.jl") include(t) ``` +In reality, we benchmarked the models also in Julia>=1.11 and from that version on a `Union` is considerably +more performant. Though, there is still a general 1.5-2x advantage in many cases in favour of [`@multiagent`](@ref), +so we suggest to use [`@multiagent`](@ref) only when the speed of the multi-agent simulation is really critical. + + From 1c9f62e50fac9161a2d08a5df7ebaa35a1dd09ca Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:17:31 +0200 Subject: [PATCH 33/54] Rename sum_faster_than_multi.jl to multiagent_vs_union.jl --- .../{sum_faster_than_multi.jl => multiagent_vs_union.jl} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/performance/{sum_faster_than_multi.jl => multiagent_vs_union.jl} (100%) diff --git a/test/performance/sum_faster_than_multi.jl b/test/performance/multiagent_vs_union.jl similarity index 100% rename from test/performance/sum_faster_than_multi.jl rename to test/performance/multiagent_vs_union.jl From 2d9f1d1352def99ad59697492a4c208bb2eba903 Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:19:31 +0200 Subject: [PATCH 34/54] Update performance_tips.md --- docs/src/performance_tips.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/src/performance_tips.md b/docs/src/performance_tips.md index ac7c6be22c..2fb0c94d97 100644 --- a/docs/src/performance_tips.md +++ b/docs/src/performance_tips.md @@ -117,7 +117,7 @@ Specifically, you can represent this property as a standard Julia `Array` that i For an example of how this is done, see the [Forest fire](@ref) model, which is a cellular automaton that has no agents in it, or the [Daisyworld](@ref) model, which has both agents as well as a spatial property represented by an `Array`. -## [Multiple agent types: `@multiagent` versus `Union` types](@id sum_vs_union) +## [Multiple agent types: `@multiagent` versus `Union` types](@id multi_vs_union) Due to the way Julia's type system works, and the fact that agents are grouped in a container mapping IDs to agent instances, using a `Union` for different agent types always creates a performance hit because it leads to type instability. @@ -152,4 +152,3 @@ In reality, we benchmarked the models also in Julia>=1.11 and from that version more performant. Though, there is still a general 1.5-2x advantage in many cases in favour of [`@multiagent`](@ref), so we suggest to use [`@multiagent`](@ref) only when the speed of the multi-agent simulation is really critical. - From 1fb11a836ad07be3d9f7ffa60b0192340b167b64 Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:25:15 +0200 Subject: [PATCH 35/54] Update tutorial.jl --- docs/src/tutorial.jl | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/docs/src/tutorial.jl b/docs/src/tutorial.jl index 6637658774..7e28738782 100644 --- a/docs/src/tutorial.jl +++ b/docs/src/tutorial.jl @@ -731,19 +731,17 @@ model = StandardABM( agent_step! ) -# ## Multiple agent types with `@sumtype` +# ## Multiple agent types with `@multiagent` -# By using `@sumtype` from `DynamicSumTypes.jl` it is possible to improve the -# computational performance of simulations requiring multiple types, while almost -# everything works the same +# By using `@multiagent` it is often possible to improve the +# computational performance of simulations requiring multiple types, +# while almost everything works the same -using DynamicSumTypes +@multiagent MultiSchelling(Schelling, Politician) <: AbstractAgent -@sumtype MultiSchelling(Schelling, Politician) <: AbstractAgent +# Now you can create instances with -# Now when you create instances you will need to enclose them in `MultiSchelling` - -p = MultiSchelling(Politician(model; pos = random_position(model), preferred_demographic = 1)) +p = MultiSchelling'.Politician(model; pos = random_position(model), preferred_demographic = 1) # agents are then all of type `MultiSchelling` @@ -751,7 +749,7 @@ typeof(p) # and hence you can't use only `typeof` to differentiate them. But you can use -typeof(variant(p)) +variantof(p) # instead. Hence, the agent stepping function should become something like @@ -768,18 +766,16 @@ end # and you need to give `MultiSchelling` as the type of agents in model initialization model = StandardABM( - MultiSchelling, # the sum type is given as the type + MultiSchelling, # the multiagent type is given as the type space; agent_step! ) # ## Adding agents of different types to the model -# Regardless of whether you went down the `Union` or `@sumtype` route, +# Regardless of whether you went down the `Union` or `@multiagent` route, # the API of Agents.jl has been designed such that there is no difference in subsequent -# usage except when adding agents to a model, when doing that with `@sumtype` it is only -# possible to use the existing [`add_agent!`](@ref) command adding an already defined -# agent. +# usage. # For example, in the union case we provide the `Union` type when we create the model, @@ -797,18 +793,18 @@ add_agent_single!(Politician, model; preferred_demographic = 1) collect(allagents(model)) -# For the `@sumtype` case instead +# For the `@multiagent` case instead model = StandardABM(MultiSchelling, space) -# we add agents like so +# and we can add agents like so -agent = MultiSchelling(Schelling(model; pos = random_position(model), group = 1, mood = true)) +agent = MultiSchelling'.Schelling(model; pos = random_position(model), group = 1, mood = true) add_agent_single!(agent, model) # or -agent = MultiSchelling(Politician(model; pos = random_position(model), preferred_demographic = 1)) +agent = MultiSchelling'.Politician(model; pos = random_position(model), preferred_demographic = 1) add_agent_single!(agent, model) # and we see From 44bccc0e757ac3b73bbc7259a639c27ea5835aa4 Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:28:50 +0200 Subject: [PATCH 36/54] Update tutorial.md --- docs/src/tutorial.md | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index 01e7bbec1d..1eb4a02019 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -764,22 +764,20 @@ model = StandardABM( ) ```` -## Multiple agent types with `@sumtype` +## Multiple agent types with `@multiagent` -By using `@sumtype` from `DynamicSumTypes.jl` it is possible to improve the -computational performance of simulations requiring multiple types, while almost -everything works the same +By using `@multiagent` it is often possible to improve the +computational performance of simulations requiring multiple types, +while almost everything works the same ````@example tutorial -using DynamicSumTypes - -@sumtype MultiSchelling(Schelling, Politician) <: AbstractAgent +@multiagent MultiSchelling(Schelling, Politician) <: AbstractAgent ```` Now when you create instances you will need to enclose them in `MultiSchelling` ````@example tutorial -p = MultiSchelling(Politician(model; pos = random_position(model), preferred_demographic = 1)) +p = MultiSchelling'.Politician(model; pos = random_position(model), preferred_demographic = 1) ```` agents are then all of type `MultiSchelling` @@ -791,7 +789,7 @@ typeof(p) and hence you can't use only `typeof` to differentiate them. But you can use ````@example tutorial -typeof(variant(p)) +variantof(p) ```` instead. Hence, the agent stepping function should become something like @@ -812,7 +810,7 @@ and you need to give `MultiSchelling` as the type of agents in model initializat ````@example tutorial model = StandardABM( - MultiSchelling, # the sum type is given as the type + MultiSchelling, # the multiagent type is given as the type space; agent_step! ) @@ -820,11 +818,9 @@ model = StandardABM( ## Adding agents of different types to the model -Regardless of whether you went down the `Union` or `@sumtype` route, +Regardless of whether you went down the `Union` or `@multiagent` route, the API of Agents.jl has been designed such that there is no difference in subsequent -usage except when adding agents to a model, when doing that with `@sumtype` it is only -possible to use the existing infrastructure for adding agents with an already defined -agent. +usage. For example, in the union case we provide the `Union` type when we create the model, @@ -850,23 +846,23 @@ and we see collect(allagents(model)) ```` -For the `@sumtype` case instead +For the `@multiagent` case instead ````@example tutorial model = StandardABM(MultiSchelling, space) ```` -we add agents like so +and we can add agents like so ````@example tutorial -agent = MultiSchelling(Schelling(model; pos = random_position(model), group = 1, mood = true)) +agent = MultiSchelling'.Schelling(model; pos = random_position(model), group = 1, mood = true) add_agent_single!(agent, model) ```` or ````@example tutorial -agent = MultiSchelling(Politician(model; pos = random_position(model), preferred_demographic = 1)) +agent = MultiSchelling'.Politician(model; pos = random_position(model), preferred_demographic = 1) add_agent_single!(agent, model) ```` From be6d4bd744f92ba2173e0178c02cfc46e87a639c Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:33:51 +0200 Subject: [PATCH 37/54] Update CHANGELOG.md --- CHANGELOG.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd48de9090..129cff78a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# v6.1 + +- The version of `@multiagent` introduced in Agents.jl v6.0 has been deprecated because a better metodology to run multiagent simulation efficiently was found. For this reason, a new + version was also introduced, its working is described in the tutorial and in the performance tips of the documentation. + # v6 - New Major release! ## Potentially BREAKING changes @@ -18,7 +23,12 @@ _We tried to deprecate every major change, resulting in practically no breakage - Every aspect of Agents.jl is orthogonal to `AgentBasedModel`: movement and neighbor searching in any space, data collection, visualizations, etc., are independent of the specific type of `AgentBasedModel` and work out of the box with any model. - Logic of when to collect data in `run!` has been improved to accommodate both discrete and continuous time models. This is reflected in the new options for the keyword `when`. - A new keyword `init` is now available for `run!` to data collect from the model before evolving it. Whether data were collected at time 0 or not was not really obvious in the original version of `run!` due to the ambiguity of the previous handling of `when`. -- Integration with `DynamicSumTypes.@sumtype` macro allows to run multi-agent simulations much more efficiently. +- A new `@multiagent` macro allows to run multi-agent simulations much more efficiently. It has + two version: In `:opt_speed` the created agents are optimized such as there is virtually + no performance difference between having 1 agent type at the cost of each agent occupying + more memory that in the `Union` case. In `:opt_memory` each agent is optimized to occupy practically + the same memory as the `Union` case, however this comes at a cost of performance versus having 1 type. + `@multiagent` kinds support multiple dispatch like syntax with the `@dispatch` macro. - A new experimental model type `EventQueueABM` has been implemented. It operates in continuous time through the scheduling of events at arbitrary time points, in contrast with the discrete time nature of a `StandardABM`. - Both the visualization and the model abstract interface have been refactored to improve the user From 782f0be2469c731ece110f794a887b62e29faf82 Mon Sep 17 00:00:00 2001 From: Tortar Date: Tue, 30 Jul 2024 20:04:48 +0200 Subject: [PATCH 38/54] improvements --- docs/src/index.md | 2 +- docs/src/tutorial.jl | 2 +- docs/src/tutorial.md | 2 +- examples/rabbit_fox_hawk.jl | 3 +- src/Agents.jl | 1 + src/core/agents.jl | 54 +++++++++++++++++++ src/core/model_event_queue.jl | 2 +- src/core/model_standard.jl | 2 +- test/performance/multiagent_vs_union.jl | 14 +++-- .../variable_agent_types_simple_dynamics.jl | 20 +++---- 10 files changed, 77 insertions(+), 25 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index c42de7197b..7ab760a2a8 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -16,7 +16,7 @@ using CairoMakie, Agents Please see the online [CHANGELOG](https://github.com/JuliaDynamics/Agents.jl/blob/main/CHANGELOG.md) for a full list of changes. The most noteworthy ones are: - - Integration with `DynamicSumTypes.@sumtype` allows to run multi-agent simulations much more efficiently. + - A new macro `@multiagent` allows to run multi-agent simulations more efficiently. - A new experimental model type `EventQueueABM` has been implemented. It operates in continuous time through the scheduling of events at arbitrary time points. It is a generalization of "Gillespie-like" models. - `AgentBasedModel` defines an API that can be extended by other models. - Stronger inheritance capabilities in `@agent`. diff --git a/docs/src/tutorial.jl b/docs/src/tutorial.jl index 7e28738782..fd5adff3df 100644 --- a/docs/src/tutorial.jl +++ b/docs/src/tutorial.jl @@ -680,7 +680,7 @@ adf # In realistic modelling situations it is often the case the the ABM is composed # of different types of agents. Agents.jl supports two approaches for multi-agent ABMs. -# The first uses the `Union` type (this subsection), and the second uses the [`@sumtype`](@ref) +# The first uses the `Union` type (this subsection), and the second uses the [`@multiagent`](@ref) # command (next subsection) available in [DynamicSumTypes.jl](https://github.com/JuliaDynamics/DynamicSumTypes.jl). # This approach is recommended as default, because in many cases it will have performance advantages # over the `Union` approach without having tangible disadvantages. However, we strongly recommend you diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index 1eb4a02019..ce2d36dc33 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -708,7 +708,7 @@ _**And this concludes the main tutorial!**_ In realistic modelling situations it is often the case the the ABM is composed of different types of agents. Agents.jl supports two approaches for multi-agent ABMs. -The first uses the `Union` type (this subsection), and the second uses the [`@sumtype`](@ref) +The first uses the `Union` type (this subsection), and the second uses the [`@multiagent`](@ref) command (next subsection) available in [DynamicSumTypes.jl](https://github.com/JuliaDynamics/DynamicSumTypes.jl). This approach is recommended as default, because in many cases it will have performance advantages over the `Union` approach without having tangible disadvantages. However, we strongly recommend you diff --git a/examples/rabbit_fox_hawk.jl b/examples/rabbit_fox_hawk.jl index b6272448af..084bdaafca 100644 --- a/examples/rabbit_fox_hawk.jl +++ b/examples/rabbit_fox_hawk.jl @@ -21,7 +21,6 @@ # and reproduce. Eating food (grass or rabbits) replenishes `energy` by a fixed amount. using Agents, Agents.Pathfinding using Random -using DynamicSumTypes import ImageMagick using FileIO: load @@ -37,7 +36,7 @@ end energy::Float64 end -@sumtype Animal(Rabbit, Fox, Hawk) <: AbstractAgent +@multiagent Animal(Rabbit, Fox, Hawk) <: AbstractAgent # A utility function to find the euclidean norm of a Vector eunorm(vec) = √sum(vec .^ 2) diff --git a/src/Agents.jl b/src/Agents.jl index 8c5857c813..c4494205fa 100644 --- a/src/Agents.jl +++ b/src/Agents.jl @@ -14,6 +14,7 @@ using Graphs using DataFrames using MacroTools using DynamicSumTypes +export variant, variantof import ProgressMeter using Random using StaticArrays: SVector diff --git a/src/core/agents.jl b/src/core/agents.jl index 238d091def..7964808204 100644 --- a/src/core/agents.jl +++ b/src/core/agents.jl @@ -253,6 +253,60 @@ function compute_base_fields(base_type_spec) return base_fields end +########################################################################################### +# @multiagent +########################################################################################### + +""" + @multiagent YourAgentType(AgentTypesToMerge) [<: OptionalSupertype] + +Define multiple agent "subtypes", which are variants of a unique type `YourAgentType`. +This means that all "subtypes" are enclosed in the overarching type. Then, You cannot +distinguish them on the basis of `typeof`, but need to use instead the `variantof` +function. The `allvariants` function for a convenient way to obtain all variants types. + +See the [Tutorial](@ref) or the [performance comparison versus `Union` types](@ref multiagent_vs_union) +for why it is often better to use `@multiagent` than making multiple agent types. + +## Examples +Let's say you have this definition: +``` +@agent struct Wolf + energy::Float64 = 0.5 + ground_speed::Float64 + const fur_color::Symbol +end +@agent struct Hawk{T} + energy::Float64 = 0.1 + ground_speed::Float64 + flight_speed::T +end + +@multiagent Animal(Wolf, Hawk{Float64}) +``` + +Then you can create `Wolf` and `Hawk` agents like so +``` +hawk_1 = Animal'.Hawk(1, (1, 1), 1.0, 2.0, 3) +hawk_2 = Animal'.Hawk(; id = 2, pos = (1, 2), ground_speed = 2.3, flight_speed = 2) +wolf_1 = Animal'.Wolf(3, (2, 2), 2.0, 3.0, :black) +wolf_2 = Animal'.Wolf(; id = 4, pos = (2, 1), ground_speed = 2.0, fur_color = :white) +``` + +The way to retrieve the variant of the agent is through the function `variantof` e.g. +``` +variantof(hawk_1) # Hawk +variantof(wolf_2) # Wolf +``` + +You can also access the enclosed variant instance with the variant `function` +``` +variant(hawk_1) # Hawk(1, (1, 1), 1.0, 2.0, 3.0) +variant(wolf_1) # Wolf(3, (2, 2), 2.0, 3.0, :black) +``` + +See the [rabbit_fox_hawk](@ref) example to see how to use this macro in a model. +""" macro multiagent(typedef) if typedef.head == :struct @warn "This version of @multiagent is deprecated because the underlying package diff --git a/src/core/model_event_queue.jl b/src/core/model_event_queue.jl index e7dfb477f7..b38f81a514 100644 --- a/src/core/model_event_queue.jl +++ b/src/core/model_event_queue.jl @@ -113,7 +113,7 @@ Here is how to construct an `EventQueueABM`: Create an instance of an [`EventQueueABM`](@ref). The model expects agents of type `AgentType(s)` living in the given `space`. -`AgentType(s)` is the result of [`@agent`](@ref) or `@sumtype` or +`AgentType(s)` is the result of [`@agent`](@ref) or `@multiagent` or a `Union` of agent types. `space` is a subtype of `AbstractSpace`, see [Space](@ref Space) for all available spaces. diff --git a/src/core/model_standard.jl b/src/core/model_standard.jl index 2d32db017a..3fcce1bf14 100644 --- a/src/core/model_standard.jl +++ b/src/core/model_standard.jl @@ -54,7 +54,7 @@ To construct a `StandardABM` use the syntax: StandardABM(AgentType(s) [, space]; properties, agent_step!, model_step!, kwargs...) The model expects agents of type `AgentType(s)` living in the given `space`. -`AgentType(s)` is the result of [`@agent`](@ref) or `@sumtype` or +`AgentType(s)` is the result of [`@agent`](@ref) or `@multiagent` or a `Union` of agent types. `space` is a subtype of `AbstractSpace`, see [Space](@ref Space) for all available spaces. diff --git a/test/performance/multiagent_vs_union.jl b/test/performance/multiagent_vs_union.jl index 2f3588b4e8..e9c64d7ba6 100644 --- a/test/performance/multiagent_vs_union.jl +++ b/test/performance/multiagent_vs_union.jl @@ -1,7 +1,7 @@ # This file compares approaching multi-agent models in two ways: # 1) using different Types to represent different agents. This leads to type # instabilities. -# 2) using @sumtype to enclose all types in a single one. This removes type +# 2) using @multiagent to enclose all types in a single one. This removes type # instabilities. # The result is that (2) is much faster. @@ -93,8 +93,6 @@ end ################### DEFINITION 2 ############### -using DynamicSumTypes - agent_step!(agent, model2) = agent_step!(agent, model2, variant(agent)) agent_step!(agent, model2, ::GridAgentOne) = randomwalk!(agent, model2) @@ -123,7 +121,7 @@ function agent_step!(agent, model2, ::GridAgentSix) agent.eight += sum(rand(abmrng(model2), (0, 1)) for a in nearby_agents(agent, model2)) end -@sumtype GridAgentAll( +@multiagent GridAgentAll( GridAgentOne, GridAgentTwo, GridAgentThree, GridAgentFour, GridAgentFive, GridAgentSix ) <: AbstractAgent @@ -157,16 +155,16 @@ t2 = @belapsed step!($model2, 50) # Results: # multiple types: 1.849 s (34267900 allocations: 1.96 GiB) -# @sumtype: 545.119 ms (22952850 allocations: 965.93 MiB) +# @multiagent: 545.119 ms (22952850 allocations: 965.93 MiB) m1 = Base.summarysize(model1) m2 = Base.summarysize(model2) # Results: # multiple types: 543.496 KiB -# @sumtype: 546.360 KiB +# @multiagent: 546.360 KiB println("Time to step the model with multiple types: $(t1) s") -println("Time to step the model with @sumtype: $(t2) s") +println("Time to step the model with @multiagent: $(t2) s") println("Memory occupied by the model with multiple types: $(m1/1000) Kib") -println("Memory occupied by the model with @sumtype: $(m2/1000) Kib") +println("Memory occupied by the model with @multiagent: $(m2/1000) Kib") diff --git a/test/performance/variable_agent_types_simple_dynamics.jl b/test/performance/variable_agent_types_simple_dynamics.jl index fdd5a844a4..6dd5aed37e 100644 --- a/test/performance/variable_agent_types_simple_dynamics.jl +++ b/test/performance/variable_agent_types_simple_dynamics.jl @@ -6,7 +6,7 @@ # are split in a varying number of agents. It shows how much of a # performance hit is to have many different agent types. -using Agents, DynamicSumTypes, Random, BenchmarkTools +using Agents, Random, BenchmarkTools @agent struct Agent1(GridAgent{2}) money::Int @@ -68,13 +68,13 @@ end money::Int end -@sumtype AgentAll2(Agent1, Agent2) <: AbstractAgent -@sumtype AgentAll3(Agent1, Agent2, Agent3) <: AbstractAgent -@sumtype AgentAll4(Agent1, Agent2, Agent3, Agent4) <: AbstractAgent -@sumtype AgentAll5(Agent1, Agent2, Agent3, Agent4, Agent5) <: AbstractAgent -@sumtype AgentAll10(Agent1, Agent2, Agent3, Agent4, Agent5, Agent6, +@multiagent AgentAll2(Agent1, Agent2) <: AbstractAgent +@multiagent AgentAll3(Agent1, Agent2, Agent3) <: AbstractAgent +@multiagent AgentAll4(Agent1, Agent2, Agent3, Agent4) <: AbstractAgent +@multiagent AgentAll5(Agent1, Agent2, Agent3, Agent4, Agent5) <: AbstractAgent +@multiagent AgentAll10(Agent1, Agent2, Agent3, Agent4, Agent5, Agent6, Agent7, Agent8, Agent9, Agent10) <: AbstractAgent -@sumtype AgentAll15(Agent1, Agent2, Agent3, Agent4, Agent5, Agent6, +@multiagent AgentAll15(Agent1, Agent2, Agent3, Agent4, Agent5, Agent6, Agent7, Agent8, Agent9, Agent10, Agent11, Agent12, Agent13, Agent14, Agent15) <: AbstractAgent function initialize_model_1(;n_agents=600,dims=(5,5)) @@ -184,15 +184,15 @@ end println("relative time of model with 1 type: 1.0") for (n, t1, t2) in zip(n_types, times_n, times_multi_s) println("relative time of model with $n types: $t1") - println("relative time of model with $n @sumtype: $t2") + println("relative time of model with $n @multiagent: $t2") end using CairoMakie fig, ax = CairoMakie.scatterlines(n_types, times_n; label = "Union"); -scatterlines!(ax, n_types, times_multi_s; label = "@sumtype") +scatterlines!(ax, n_types, times_multi_s; label = "@multiagent") ax.xlabel = "# types" ax.ylabel = "time relative to 1 type" -ax.title = "Union types vs @sumtype" +ax.title = "Union types vs @multiagent" axislegend(ax; position = :lt) ax.yticks = 0:1:ceil(Int, maximum(times_n)) ax.xticks = [2, 3, 4, 5, 10, 15] From 6099eb4c68be6472653ecbde26f20832b337e321 Mon Sep 17 00:00:00 2001 From: Tortar Date: Tue, 30 Jul 2024 20:06:59 +0200 Subject: [PATCH 39/54] fix --- src/core/agents.jl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/core/agents.jl b/src/core/agents.jl index 7964808204..cbdbe581bb 100644 --- a/src/core/agents.jl +++ b/src/core/agents.jl @@ -315,6 +315,13 @@ macro multiagent(typedef) model to use the new methodology." return esc(_multiagent(QuoteNode(:opt_speed), typedef)) else - return esc(:($DynamicSumTypes.@sumtype $typedef)) + if typedef.head === :call + abstract_type = :AbstractAgent + type_with_variants = typedef + elseif typedef.head === :(<:) + abstract_type = typedef.args[2] + type_with_variants = typedef.args[1] + end + return esc(:($DynamicSumTypes.@sumtype $type_with_variants <: $abstract_type)) end end From 7419e999986a8dcd74cdf6b12a883b2085256913 Mon Sep 17 00:00:00 2001 From: Tortar Date: Tue, 30 Jul 2024 20:11:48 +0200 Subject: [PATCH 40/54] fix --- CHANGELOG.md | 4 ++-- examples/rabbit_fox_hawk.jl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 129cff78a1..5520aa06e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # v6.1 -- The version of `@multiagent` introduced in Agents.jl v6.0 has been deprecated because a better metodology to run multiagent simulation efficiently was found. For this reason, a new - version was also introduced, its working is described in the tutorial and in the performance tips of the documentation. +- The version of `@multiagent` introduced in Agents.jl v6.0 has been deprecated because a better metodology to run multiagent simulation efficiently was found. For this reason, +a new version was also introduced, its working is described in the tutorial and in the performance tips of the documentation. # v6 - New Major release! diff --git a/examples/rabbit_fox_hawk.jl b/examples/rabbit_fox_hawk.jl index 084bdaafca..4dfa6ec95a 100644 --- a/examples/rabbit_fox_hawk.jl +++ b/examples/rabbit_fox_hawk.jl @@ -36,7 +36,7 @@ end energy::Float64 end -@multiagent Animal(Rabbit, Fox, Hawk) <: AbstractAgent +@multiagent Animal(Rabbit, Fox, Hawk) # A utility function to find the euclidean norm of a Vector eunorm(vec) = √sum(vec .^ 2) From 499d40f9f6af09a68cbdbc03e77e7485f789f9ee Mon Sep 17 00:00:00 2001 From: Tortar Date: Tue, 30 Jul 2024 20:12:57 +0200 Subject: [PATCH 41/54] fix --- docs/Project.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/Project.toml b/docs/Project.toml index 544bf91d5d..0758cd1e6e 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -34,7 +34,6 @@ StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" [compat] -julia = "1.11" BlackBoxOptim = "0.6" CellListMap = "0.9" Documenter = "1" From 248edec56b461f1b9da126e834c52af40783015c Mon Sep 17 00:00:00 2001 From: Tortar Date: Tue, 30 Jul 2024 20:36:06 +0200 Subject: [PATCH 42/54] f --- docs/src/tutorial.jl | 42 +--------------------- docs/src/tutorial.md | 59 +------------------------------ src/core/space_interaction_API.jl | 4 +-- src/spaces/discrete.jl | 4 +-- 4 files changed, 6 insertions(+), 103 deletions(-) diff --git a/docs/src/tutorial.jl b/docs/src/tutorial.jl index fd5adff3df..bb5852dc19 100644 --- a/docs/src/tutorial.jl +++ b/docs/src/tutorial.jl @@ -771,46 +771,6 @@ model = StandardABM( agent_step! ) -# ## Adding agents of different types to the model - # Regardless of whether you went down the `Union` or `@multiagent` route, # the API of Agents.jl has been designed such that there is no difference in subsequent -# usage. - -# For example, in the union case we provide the `Union` type when we create the model, - -model = StandardABM(Union{Schelling, Politician}, space) - -# we add them by specifying the type - -add_agent_single!(Schelling, model; group = 1, mood = true) - -# or - -add_agent_single!(Politician, model; preferred_demographic = 1) - -# and we see - -collect(allagents(model)) - -# For the `@multiagent` case instead - -model = StandardABM(MultiSchelling, space) - -# and we can add agents like so - -agent = MultiSchelling'.Schelling(model; pos = random_position(model), group = 1, mood = true) -add_agent_single!(agent, model) - -# or - -agent = MultiSchelling'.Politician(model; pos = random_position(model), preferred_demographic = 1) -add_agent_single!(agent, model) - -# and we see - -collect(allagents(model)) - -# And that's the end of the tutorial!!! -# You can visit other examples to see other types of usage of Agents.jl, -# or go into the [API](@ref) to find the functions you need to make your own ABM! +# usage. \ No newline at end of file diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index ce2d36dc33..3cf43a5fe0 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -816,63 +816,6 @@ model = StandardABM( ) ```` -## Adding agents of different types to the model - Regardless of whether you went down the `Union` or `@multiagent` route, the API of Agents.jl has been designed such that there is no difference in subsequent -usage. - -For example, in the union case we provide the `Union` type when we create the model, - -````@example tutorial -model = StandardABM(Union{Schelling, Politician}, space) -```` - -we add them by specifying the type - -````@example tutorial -add_agent_single!(Schelling, model; group = 1, mood = true) -```` - -or - -````@example tutorial -add_agent_single!(Politician, model; preferred_demographic = 1) -```` - -and we see - -````@example tutorial -collect(allagents(model)) -```` - -For the `@multiagent` case instead - -````@example tutorial -model = StandardABM(MultiSchelling, space) -```` - -and we can add agents like so - -````@example tutorial -agent = MultiSchelling'.Schelling(model; pos = random_position(model), group = 1, mood = true) -add_agent_single!(agent, model) -```` - -or - -````@example tutorial -agent = MultiSchelling'.Politician(model; pos = random_position(model), preferred_demographic = 1) -add_agent_single!(agent, model) -```` - -and we see - -````@example tutorial -collect(allagents(model)) -```` - -And that's the end of the tutorial!!! -You can visit other examples to see other types of usage of Agents.jl, -or go into the [API](@ref) to find the functions you need to make your own ABM! - +usage. \ No newline at end of file diff --git a/src/core/space_interaction_API.jl b/src/core/space_interaction_API.jl index 26ce4a3052..97501c8fa2 100644 --- a/src/core/space_interaction_API.jl +++ b/src/core/space_interaction_API.jl @@ -262,7 +262,7 @@ function add_agent!(model::ABM, args::Vararg{Any, N}; kwargs...) where {N} add_agent!(A, model, args...; kwargs...) end -function add_agent!(A::Type, model::ABM, +function add_agent!(A::Union{Function, Type}, model::ABM, args::Vararg{Any, N}; kwargs...) where {N} add_agent!(random_position(model), A, model, args...; kwargs...) end @@ -280,7 +280,7 @@ end # lowest level - actually constructs the agent function add_agent!( pos::ValidPos, - A::Type, + A::Union{Function, Type}, model::ABM, args::Vararg{Any, N}; kwargs..., diff --git a/src/spaces/discrete.jl b/src/spaces/discrete.jl index 87e9773818..d33e938dec 100644 --- a/src/spaces/discrete.jl +++ b/src/spaces/discrete.jl @@ -217,7 +217,7 @@ end Same as `add_agent!(A, model, properties...; kwargs...)` but ensures that it adds an agent into a position with no other agents (does nothing if no such position exists). """ -function add_agent_single!(A::Type, model::ABM, properties::Vararg{Any, N}; kwargs...) where {N} +function add_agent_single!(A::Union{Function, Type}, model::ABM, properties::Vararg{Any, N}; kwargs...) where {N} position = random_empty(model) isnothing(position) && return nothing agent = add_agent!(position, A, model, properties...; kwargs...) @@ -248,7 +248,7 @@ function fill_space!(model::ABM, args::Vararg{Any, N}; kwargs...) where {N} end function fill_space!( - A::Type, + A::Union{Function, Type}, model::ABM{<:DiscreteSpace}, args::Vararg{Any, N}; kwargs..., From 24f9401f13e4b9e140a1e5e53cc35453480bfe4a Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 30 Jul 2024 22:21:43 +0200 Subject: [PATCH 43/54] readd multiagent --- docs/src/api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/api.md b/docs/src/api.md index 97846797a1..6d015734a9 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -30,6 +30,7 @@ AgentEvent ```@docs @agent +@multiagent AbstractAgent ``` From 9a0ac45b9d469ed43980694cab0feb682ada27ef Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 30 Jul 2024 22:23:26 +0200 Subject: [PATCH 44/54] Update docs/src/tutorial.jl Co-authored-by: George Datseris --- docs/src/tutorial.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/tutorial.jl b/docs/src/tutorial.jl index bb5852dc19..3f6bbc35db 100644 --- a/docs/src/tutorial.jl +++ b/docs/src/tutorial.jl @@ -680,8 +680,8 @@ adf # In realistic modelling situations it is often the case the the ABM is composed # of different types of agents. Agents.jl supports two approaches for multi-agent ABMs. -# The first uses the `Union` type (this subsection), and the second uses the [`@multiagent`](@ref) -# command (next subsection) available in [DynamicSumTypes.jl](https://github.com/JuliaDynamics/DynamicSumTypes.jl). +# The first uses the `Union` type, and the second uses the [`@multiagent`](@ref) +# command. # This approach is recommended as default, because in many cases it will have performance advantages # over the `Union` approach without having tangible disadvantages. However, we strongly recommend you # to read through the [comparison of the two approaches](@ref sum_vs_union). From 7e83193aed96add6c8eb6258f3086b030e7151bc Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 30 Jul 2024 22:29:32 +0200 Subject: [PATCH 45/54] Update tutorial.jl --- docs/src/tutorial.jl | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/src/tutorial.jl b/docs/src/tutorial.jl index 3f6bbc35db..e834f8ccae 100644 --- a/docs/src/tutorial.jl +++ b/docs/src/tutorial.jl @@ -773,4 +773,40 @@ model = StandardABM( # Regardless of whether you went down the `Union` or `@multiagent` route, # the API of Agents.jl has been designed such that there is no difference in subsequent -# usage. \ No newline at end of file +# usage. + +# For example, in the union case we provide the `Union` type when we create the model, + +model = StandardABM(Union{Schelling, Politician}, space) + +# we add them by specifying the type + +add_agent_single!(Schelling, model; group = 1, mood = true) + +# or + +add_agent_single!(Politician, model; preferred_demographic = 1) + +# and we see + +collect(allagents(model)) + +# For the `@multiagent` case, there is really no difference. We have + +model = StandardABM(MultiSchelling, space) + +# we add + +add_agent_single!(MultiSchelling'.Schelling, model; group = 1) + +# or + +add_agent_single!(MultiSchelling'.Politician, model; influence = 0.5, group = 1) + +# and we see + +collect(allagents(model)) + +# And that's the end of the tutorial!!! +# You can visit other examples to see other types of usage of Agents.jl, +# or go into the [API](@ref) to find the functions you need to make your own ABM! From df38eda022b4339c97f97f5e0242fa4fce4d7eae Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 30 Jul 2024 22:30:30 +0200 Subject: [PATCH 46/54] Update tutorial.jl --- docs/src/tutorial.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/tutorial.jl b/docs/src/tutorial.jl index e834f8ccae..404e04c886 100644 --- a/docs/src/tutorial.jl +++ b/docs/src/tutorial.jl @@ -801,7 +801,7 @@ add_agent_single!(MultiSchelling'.Schelling, model; group = 1) # or -add_agent_single!(MultiSchelling'.Politician, model; influence = 0.5, group = 1) +add_agent_single!(MultiSchelling'.Politician, model; preferred_demographic = 1) # and we see From 0245c6db7bda61b83832d403c6ed42a6baab0db0 Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 30 Jul 2024 22:33:04 +0200 Subject: [PATCH 47/54] Update tutorial.md --- docs/src/tutorial.md | 54 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index 3cf43a5fe0..f8b1f4fe6e 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -818,4 +818,56 @@ model = StandardABM( Regardless of whether you went down the `Union` or `@multiagent` route, the API of Agents.jl has been designed such that there is no difference in subsequent -usage. \ No newline at end of file +usage. + +For example, in the union case we provide the `Union` type when we create the model, + +````@example tutorial +model = StandardABM(Union{Schelling, Politician}, space) +```` + +we add them by specifying the type + +````@example tutorial +add_agent_single!(Schelling, model; group = 1, mood = true) +```` + +or + +````@example tutorial +add_agent_single!(Politician, model; preferred_demographic = 1) +```` + +and we see + +````@example tutorial +collect(allagents(model)) +```` + +For the `@multiagent` case, there is really no difference. We have + +````@example tutorial +model = StandardABM(MultiSchelling, space) +```` + +we add + +````@example tutorial +add_agent_single!(MultiSchelling'.Schelling, model; group = 1) +```` + +or + +````@example tutorial +add_agent_single!(MultiSchelling'.Politician, model; preferred_demographic = 1) +```` + +and we see + +````@example tutorial +collect(allagents(model)) +```` + +And that's the end of the tutorial!!! +You can visit other examples to see other types of usage of Agents.jl, +or go into the [API](@ref) to find the functions you need to make your own ABM! From 51e95ff14cab70869ed1814e440ef8a6fa73afa9 Mon Sep 17 00:00:00 2001 From: Tortar Date: Tue, 6 Aug 2024 11:59:16 +0200 Subject: [PATCH 48/54] Add constructor function --- docs/src/tutorial.jl | 9 +++++---- docs/src/tutorial.md | 9 +++++---- src/core/agents.jl | 6 ++++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/src/tutorial.jl b/docs/src/tutorial.jl index 404e04c886..6df5db7308 100644 --- a/docs/src/tutorial.jl +++ b/docs/src/tutorial.jl @@ -741,7 +741,7 @@ model = StandardABM( # Now you can create instances with -p = MultiSchelling'.Politician(model; pos = random_position(model), preferred_demographic = 1) +p = constructor(MultiSchelling, Politician)(model; pos = random_position(model), preferred_demographic = 1) # agents are then all of type `MultiSchelling` @@ -791,17 +791,18 @@ add_agent_single!(Politician, model; preferred_demographic = 1) collect(allagents(model)) -# For the `@multiagent` case, there is really no difference. We have +# For the `@multiagent` case, there is really no difference apart from +# the usage of a custom `constructor` function. We have model = StandardABM(MultiSchelling, space) # we add -add_agent_single!(MultiSchelling'.Schelling, model; group = 1) +add_agent_single!(constructor(MultiSchelling, Schelling), model; group = 1) # or -add_agent_single!(MultiSchelling'.Politician, model; preferred_demographic = 1) +add_agent_single!(constructor(MultiSchelling, Politician), model; preferred_demographic = 1) # and we see diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index f8b1f4fe6e..6b75e3eebd 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -777,7 +777,7 @@ while almost everything works the same Now when you create instances you will need to enclose them in `MultiSchelling` ````@example tutorial -p = MultiSchelling'.Politician(model; pos = random_position(model), preferred_demographic = 1) +p = constructor(MultiSchelling, Politician)(model; pos = random_position(model), preferred_demographic = 1) ```` agents are then all of type `MultiSchelling` @@ -844,7 +844,8 @@ and we see collect(allagents(model)) ```` -For the `@multiagent` case, there is really no difference. We have +For the `@multiagent` case, there is really no difference apart from +the usage of a custom `constructor` function. We have ````@example tutorial model = StandardABM(MultiSchelling, space) @@ -853,13 +854,13 @@ model = StandardABM(MultiSchelling, space) we add ````@example tutorial -add_agent_single!(MultiSchelling'.Schelling, model; group = 1) +add_agent_single!(constructor(MultiSchelling, Schelling), model; group = 1) ```` or ````@example tutorial -add_agent_single!(MultiSchelling'.Politician, model; preferred_demographic = 1) +add_agent_single!(constructor(MultiSchelling, Politician), model; preferred_demographic = 1) ```` and we see diff --git a/src/core/agents.jl b/src/core/agents.jl index cbdbe581bb..1e96bab3f2 100644 --- a/src/core/agents.jl +++ b/src/core/agents.jl @@ -1,4 +1,4 @@ -export AbstractAgent, @agent, @multiagent, NoSpaceAgent +export AbstractAgent, @agent, @multiagent, NoSpaceAgent, constructor ########################################################################################### # @agent @@ -324,4 +324,6 @@ macro multiagent(typedef) end return esc(:($DynamicSumTypes.@sumtype $type_with_variants <: $abstract_type)) end -end +end + +constructor(MultiAgent::Type, Variant::Type) = (args...; kwargs...) -> MultiAgent(Variant(args...; kwargs...)) From f17588ee06ff53abf85e5f172942047ed40eba3d Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:05:51 +0200 Subject: [PATCH 49/54] Update CHANGELOG.md Co-authored-by: George Datseris --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5520aa06e6..917f94d271 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # v6.1 -- The version of `@multiagent` introduced in Agents.jl v6.0 has been deprecated because a better metodology to run multiagent simulation efficiently was found. For this reason, +The `@multiagent` macro introduced in Agents.jl v6.0 has been completely overhauled. We found a better methodology to create performant multi-agent types that can re-use existing agent types. Please read the docstring of the new `@multiagent` and consult the updated tutorial in v6.1 for more details. + +All `@multiagent`-specific functions that were introduced in v6.0, such as `kindof`, have also been deprecated, see the main tutorial. + +For `EventQueueABM`, that was an experimental feature that used to work only with `kindof`, it is also now changed to work with the standard `typeof` of the base Julia language. a new version was also introduced, its working is described in the tutorial and in the performance tips of the documentation. # v6 - New Major release! From 0a497df4a5c4aea4a08bfca21c440b8b0a102b3d Mon Sep 17 00:00:00 2001 From: Adriano Meligrana <68152031+Tortar@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:06:23 +0200 Subject: [PATCH 50/54] Update CHANGELOG.md Co-authored-by: George Datseris --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 917f94d271..e259c2bef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,6 @@ The `@multiagent` macro introduced in Agents.jl v6.0 has been completely overhau All `@multiagent`-specific functions that were introduced in v6.0, such as `kindof`, have also been deprecated, see the main tutorial. For `EventQueueABM`, that was an experimental feature that used to work only with `kindof`, it is also now changed to work with the standard `typeof` of the base Julia language. -a new version was also introduced, its working is described in the tutorial and in the performance tips of the documentation. # v6 - New Major release! From 50a2373a348a9337916d9703bc4a9f3082cd4c62 Mon Sep 17 00:00:00 2001 From: Tortar Date: Tue, 6 Aug 2024 16:06:39 +0200 Subject: [PATCH 51/54] address code review --- examples/event_rock_paper_scissors.jl | 28 +++++++++++++++------------ src/core/agents.jl | 8 ++++---- src/core/model_event_queue.jl | 16 ++++++++------- src/simulations/sample.jl | 2 +- 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/examples/event_rock_paper_scissors.jl b/examples/event_rock_paper_scissors.jl index fcbf1a7f51..afb0e02dae 100644 --- a/examples/event_rock_paper_scissors.jl +++ b/examples/event_rock_paper_scissors.jl @@ -1,3 +1,4 @@ + # # [Spatial rock-paper-scissors (event based)](@id eventbased_tutorial) # ```@raw html @@ -35,7 +36,7 @@ # We start by loading `Agents` -using Agents, DynamicSumTypes +using Agents # and defining the three agent types @@ -43,6 +44,10 @@ using Agents, DynamicSumTypes @agent struct Paper(GridAgent{2}) end @agent struct Scissors(GridAgent{2}) end +# we use a multiagent in the simulation, but everything works +# also with a single agents or a `Union` of agents +@multiagent RPS(Rock, Paper, Scissors) + # %% #src # Actions of events are standard Julia functions that utilize Agents.jl [API](@ref), @@ -60,14 +65,14 @@ function attack!(agent, model) isnothing(contender) && return ## else perform standard rock paper scissors logic ## and remove the contender if you win. - attack!(agent, contender, model) + attack!(variant(agent), variant(contender), contender, model) return end -attack!(::AbstractAgent, ::AbstractAgent, model) = nothing -attack!(::Rock, contender::Scissors, model) = remove_agent!(contender, model) -attack!(::Scissors, contender::Paper, model) = remove_agent!(contender, model) -attack!(::Paper, contender::Rock, model) = remove_agent!(contender, model) +attack!(::AbstractAgent, ::AbstractAgent, contender, model) = nothing +attack!(::Rock, ::Scissors, contender, model) = remove_agent!(contender, model) +attack!(::Scissors, ::Paper, contender, model) = remove_agent!(contender, model) +attack!(::Paper, ::Rock, contender, model) = remove_agent!(contender, model) # The movement function is equally simple due to # the many functions offered by Agents.jl [API](@ref). @@ -174,7 +179,6 @@ space = GridSpaceSingle((100, 100)) using Random: Xoshiro rng = Xoshiro(42) -const RPS = Union{Rock, Paper, Scissors} model = EventQueueABM(RPS, events, space; rng, warn = false) # populating the model with agents is the same as in the main [Tutorial](@ref), @@ -186,7 +190,7 @@ const alltypes = (Rock, Paper, Scissors) for p in positions(model) type = rand(abmrng(model), alltypes) - add_agent!(p, type, model) + add_agent!(p, constructor(RPS, type), model) end # We can see the list of scheduled events via @@ -209,7 +213,7 @@ function initialize_rps(; n = 100, nx = n, ny = n, seed = 42) model = EventQueueABM(RPS, events, space; rng, warn = false) for p in positions(model) type = rand(abmrng(model), alltypes) - add_agent!(p, type, model) + add_agent!(p, constructor(RPS, type), model) end return model end @@ -235,7 +239,7 @@ function terminate(model, t) ## and for each it checks if it is less than the threshold. ## if any is, it returns `true`, otherwise `false.` logic = any(alltypes) do type - n = count(a -> typeof(a) == type, allagents(model)) + n = count(a -> variantof(a) == type, allagents(model)) return n < threshold end ## For safety, in case this never happens, we also add a trigger @@ -260,7 +264,7 @@ abmtime(model) model = initialize_rps() -adata = [(a -> typeof(a) === X, count) for X in alltypes] +adata = [(a -> variantof(a) === X, count) for X in alltypes] adf, mdf = run!(model, 100.0; adata, when = 0.5, dt = 0.01) @@ -291,7 +295,7 @@ fig # corresponds to continuous time and does not have to be an integer. const colormap = Dict(Rock => "black", Scissors => "gray", Paper => "orange") -agent_color(agent) = colormap[typeof(agent)] +agent_color(agent) = colormap[variantof(agent)] plotkw = (agent_color, agent_marker = :rect, agent_size = 5) fig, ax, abmobs = abmplot(model; plotkw...) diff --git a/src/core/agents.jl b/src/core/agents.jl index 1e96bab3f2..bcbcc81cca 100644 --- a/src/core/agents.jl +++ b/src/core/agents.jl @@ -287,10 +287,10 @@ end Then you can create `Wolf` and `Hawk` agents like so ``` -hawk_1 = Animal'.Hawk(1, (1, 1), 1.0, 2.0, 3) -hawk_2 = Animal'.Hawk(; id = 2, pos = (1, 2), ground_speed = 2.3, flight_speed = 2) -wolf_1 = Animal'.Wolf(3, (2, 2), 2.0, 3.0, :black) -wolf_2 = Animal'.Wolf(; id = 4, pos = (2, 1), ground_speed = 2.0, fur_color = :white) +hawk_1 = constructor(Animal, Hawk)(1, (1, 1), 1.0, 2.0, 3) +hawk_2 = constructor(Animal, Hawk)(; id = 2, pos = (1, 2), ground_speed = 2.3, flight_speed = 2) +wolf_1 = constructor(Animal, Wolf)(3, (2, 2), 2.0, 3.0, :black) +wolf_2 = constructor(Animal, Wolf)(; id = 4, pos = (2, 1), ground_speed = 2.0, fur_color = :white) ``` The way to retrieve the variant of the agent is through the function `variantof` e.g. diff --git a/src/core/model_event_queue.jl b/src/core/model_event_queue.jl index b38f81a514..95818815c9 100644 --- a/src/core/model_event_queue.jl +++ b/src/core/model_event_queue.jl @@ -38,7 +38,7 @@ struct EventQueueABM{ S<:SpaceType, A<:AbstractAgent, C<:ContainerType{A}, - P,E,R<:AbstractRNG,ET,PT,FPT,TI,Q} <: AgentBasedModel{S} + P,E,R<:AbstractRNG,TF,ET,PT,FPT,TI,Q} <: AgentBasedModel{S} # core ABM stuff agents::C space::S @@ -48,6 +48,7 @@ struct EventQueueABM{ time::Base.RefValue{Float64} # Specific to event queue events::E + type_func::TF type_to_idx::TI idx_events_each_type::ET propensities_each_type::PT @@ -161,7 +162,8 @@ function EventQueueABM( # the queue stores pairs of (agent ID, event index) mapping them to their trigger time queue = BinaryHeap(Base.By(last), Pair{Tuple{I, Int}, Float64}[]) - agent_types = union_types(A) + agent_types = is_sumtype(A) ? values(allvariants(A)) : union_types(A) + type_func = is_sumtype(A) ? variantof : typeof type_to_idx = Dict(t => i for (i, t) in enumerate(agent_types)) # precompute a vector mapping the agent type index to a @@ -193,15 +195,15 @@ function EventQueueABM( # because we use the index of `kind_to_index` to access them. # construct the type - ET,PT,FPT,TI,Q = typeof.(( - idx_events_each_type, propensities_each_type, + TF,ET,PT,FPT,TI,Q = typeof.(( + type_func, idx_events_each_type, propensities_each_type, idx_func_propensities_each_type, type_to_idx, queue )) - return EventQueueABM{S,A,C,P,E,R,ET,PT,FPT,TI,Q}( + return EventQueueABM{S,A,C,P,E,R,TF,ET,PT,FPT,TI,Q}( agents, space, properties, rng, Ref(0), Ref(0.0), - events, type_to_idx, idx_events_each_type, + events, type_func, type_to_idx, idx_events_each_type, propensities_each_type, idx_func_propensities_each_type, queue, autogenerate_on_add, autogenerate_after_action, ) @@ -234,7 +236,7 @@ current time of the `model`. function add_event!(agent, model) # TODO: Study type stability of this function events = abmevents(model) # Here, we retrieve the applicable events for the agent and corresponding info - idx = getfield(model, :type_to_idx)[typeof(agent)] + idx = getfield(model, :type_to_idx)[getfield(model, :type_func)(agent)] events_type = getfield(model, :idx_events_each_type)[idx] propensities_type = getfield(model, :propensities_each_type)[idx] idx_func_propensities_type = getfield(model, :idx_func_propensities_each_type)[idx] diff --git a/src/simulations/sample.jl b/src/simulations/sample.jl index 0f029dbc03..f74c6363b0 100644 --- a/src/simulations/sample.jl +++ b/src/simulations/sample.jl @@ -110,7 +110,7 @@ function copy_agent(agent::A, model, id_new; kwargs...) where {A<:AbstractAgent} newagent = variant_constructor(agent)(id_new, args...) elseif is_sumtype(A) args = new_args_sum_t(agent, model; kwargs...) - newagent = A(variant(agent)(id_new, args...)) + newagent = A(variantof(agent)(id_new, args...)) else args = new_args_t(agent, model; kwargs...) newagent = A(id_new, args...) From 3c0f700ebfa63b755a011477b6ba8388885c520b Mon Sep 17 00:00:00 2001 From: Tortar Date: Tue, 6 Aug 2024 21:24:30 +0200 Subject: [PATCH 52/54] a --- examples/event_rock_paper_scissors.jl | 2 +- examples/rabbit_fox_hawk.jl | 2 +- src/core/agents.jl | 2 +- src/core/model_event_queue.jl | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/event_rock_paper_scissors.jl b/examples/event_rock_paper_scissors.jl index afb0e02dae..a92e72df72 100644 --- a/examples/event_rock_paper_scissors.jl +++ b/examples/event_rock_paper_scissors.jl @@ -334,4 +334,4 @@ for _ in 1:100 # this loop simulates pressing the `run!` button step!(abmobs, 1.0) end -fig +fig \ No newline at end of file diff --git a/examples/rabbit_fox_hawk.jl b/examples/rabbit_fox_hawk.jl index 4dfa6ec95a..f274ee1edb 100644 --- a/examples/rabbit_fox_hawk.jl +++ b/examples/rabbit_fox_hawk.jl @@ -1,4 +1,4 @@ -# # 3D Mixed-Agent Ecosystem with Pathfinding +# # [3D Mixed-Agent Ecosystem with Pathfinding](@id rabbit_fox_hawk) # ```@raw html #