Skip to content

Commit

Permalink
Update to DynamicSumTypes 3: remove @multiagent
Browse files Browse the repository at this point in the history
  • Loading branch information
Tortar committed Jul 7, 2024
1 parent 051f62b commit d3c4d47
Show file tree
Hide file tree
Showing 9 changed files with 53 additions and 373 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 0 additions & 5 deletions docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,6 @@ AgentEvent
```@docs
@agent
AbstractAgent
@multiagent
kindof
allkinds
@dispatch
@finalize_dispatch
```

### Minimal agent types
Expand Down
38 changes: 9 additions & 29 deletions examples/event_rock_paper_scissors.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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).
Expand Down Expand Up @@ -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.
Expand Down
1 change: 0 additions & 1 deletion src/Agents.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ using Graphs
using DataFrames
using MacroTools
using DynamicSumTypes
export DynamicSumTypes
import ProgressMeter
using Random
using StaticArrays: SVector
Expand Down
227 changes: 1 addition & 226 deletions src/core/agents.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export AbstractAgent, @agent, @multiagent, @dispatch, @finalize_dispatch, NoSpaceAgent, kindof, allkinds
using DynamicSumTypes: allkinds
export AbstractAgent, @agent, NoSpaceAgent

###########################################################################################
# @agent
Expand Down Expand Up @@ -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)
Expand All @@ -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
Loading

0 comments on commit d3c4d47

Please sign in to comment.