From d3c4d47b858f278a17243856624ea23bd7188c61 Mon Sep 17 00:00:00 2001 From: Tortar Date: Sun, 7 Jul 2024 03:48:01 +0200 Subject: [PATCH] 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