Skip to content

Commit

Permalink
Update to DynamicSumTypes 3 (#1055)
Browse files Browse the repository at this point in the history
* Update to DynamicSumTypes 3: remove @multiagent

* fixes

* update other benchmark

* more

* Update Project.toml

* Update tutorial.md

* Update tutorial.jl

* Update tutorial.jl

* Update tutorial.md

* Update tutorial.jl

* Update tutorial.md

* Update tutorial.md

* Update tutorial.md

* Update tutorial.jl

* Update performance_tips.md

* Update performance_tips.md

* Update sum_faster_than_multi.jl

* Update sum_faster_than_multi.jl

* Update sum_faster_than_multi.jl

* Update sum_faster_than_multi.jl

* Update Project.toml

* Update ci.yml

* Update ci.yml

* Update ci.yml

* Update ci.yml

* Update ci.yml

* Update agents.jl

* Update deprecations.jl

* Update agents.jl

* Update deprecations.jl

* Update agents.jl

* Update performance_tips.md

* Rename sum_faster_than_multi.jl to multiagent_vs_union.jl

* Update performance_tips.md

* Update tutorial.jl

* Update tutorial.md

* Update CHANGELOG.md

* improvements

* fix

* fix

* fix

* f

* readd multiagent

* Update docs/src/tutorial.jl

Co-authored-by: George Datseris <[email protected]>

* Update tutorial.jl

* Update tutorial.jl

* Update tutorial.md

* Add constructor function

* Update CHANGELOG.md

Co-authored-by: George Datseris <[email protected]>

* Update CHANGELOG.md

Co-authored-by: George Datseris <[email protected]>

* address code review

* a

* ex

* ex2

---------

Co-authored-by: George Datseris <[email protected]>
  • Loading branch information
Tortar and Datseris authored Aug 6, 2024
1 parent 62601fb commit 6c98464
Show file tree
Hide file tree
Showing 21 changed files with 469 additions and 710 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# v6.1

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.

# v6 - New Major release!

## Potentially BREAKING changes
Expand All @@ -23,7 +31,7 @@ _We tried to deprecate every major change, resulting in practically no breakage
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.
`@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
Expand Down
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -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.17"
version = "6.1.0"

[deps]
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
Expand Down 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
1 change: 1 addition & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 1 addition & 5 deletions docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,8 @@ AgentEvent

```@docs
@agent
AbstractAgent
@multiagent
kindof
allkinds
@dispatch
@finalize_dispatch
AbstractAgent
```

### Minimal agent types
Expand Down
2 changes: 1 addition & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
- 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`.
Expand Down
24 changes: 18 additions & 6 deletions docs/src/performance_tips.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,11 @@ For an example of how this is done, see the [Forest fire](@ref) model, which is
## [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.
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 [`@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 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
Expand All @@ -138,5 +135,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 and allows you to utilize multiple dispatch.
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 [`@multiagent`](@ref) macro.

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", "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.

158 changes: 39 additions & 119 deletions docs/src/tutorial.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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).

# _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
Expand All @@ -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.
Expand All @@ -708,161 +707,81 @@ 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
)

# 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

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::Schelling, model)
## stuff.
end

function agent_step!(agent::Politician, model)
## other stuff.
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!
agent_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)

# however, only one of these names is an actual Julia type:
# By using `@multiagent` it is often possible to improve the
# computational performance of simulations requiring multiple types,
# while almost everything works the same

fieldnames(MultiSchelling)
@multiagent MultiSchelling(Schelling, Politician) <: AbstractAgent

# that contains all fields of all subtypes without duplication, while
# Now you can create instances with

fieldnames(Civilian)
p = constructor(MultiSchelling, Politician)(model; pos = random_position(model), preferred_demographic = 1)

# 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**.
# agents are then all of type `MultiSchelling`

# E.g., you can initialize
typeof(p)

civ = Civilian(; id = 2, pos = (2, 2), group = 2) # default `mood`
# and hence you can't use only `typeof` to differentiate them. But you can use

# 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`

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:

function multi_step!(agent, model)
if kindof(agent) == :Civilian
civilian_step!(agent, model)
elseif kindof(agent) == :Governor
politician_step!(agent, model)
end
end
variantof(p)

function civilian_step!(agent, model)
## stuff.
end

function politician_step!(agent, model)
## other stuff.
end
# instead. Hence, the agent stepping function should become something like

# This however can be made to look much more like multiple dispatch with the
# introduction of another macro, `@dispatch`:
agent_step!(agent, model) = agent_step!(agent, model, variant(agent))

@dispatch function multi_step!(agent::Civilian, model)
function agent_step!(agent, model, ::Schelling)
## stuff.
end

@dispatch function multi_step!(agent::Politician, model)
function agent_step!(agent, model, ::Politician)
## other 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.
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 multiagent type is given as the type
space;
agent_step! = multi_step!
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. 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.

# 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

Expand All @@ -872,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!(Civilian, model; group = 1)
add_agent_single!(constructor(MultiSchelling, Schelling), model; group = 1)

# or

add_agent_single!(Governor, model; influence = 0.5, group = 1)
add_agent_single!(constructor(MultiSchelling, Politician), model; preferred_demographic = 1)

# and we see

Expand Down
Loading

0 comments on commit 6c98464

Please sign in to comment.