Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
Datseris committed Jun 11, 2020
2 parents b889506 + cf52be3 commit ac5b93a
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 20 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# v3.1
## Additions
- Extend `interacting_pairs` to allow interactions of disparate types when using mixed models.

# v3.0
## Additions
* Added `ContinuousSpace` as a space option. Supports Euclidean and Cityblock metrics. Several new API functions were added for continuous space.
Expand Down
2 changes: 1 addition & 1 deletion 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 = ["Ali Vahdati", "George Datseris", "Tim DuBois"]
version = "3.0.0"
version = "3.1.1"

[deps]
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Expand Down
2 changes: 1 addition & 1 deletion docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ return df_agent, df_model
```
(here `until` and `should_we_collect` are internal functions)

## Schedulers
## [Schedulers](@id Schedulers)
The schedulers of Agents.jl have a very simple interface. All schedulers are functions,
that take as an input the ABM and return an iterator over agent IDs.
Notice that this iterator can be a "true" iterator or can be just a standard vector of IDs.
Expand Down
9 changes: 5 additions & 4 deletions examples/schelling.jl
Original file line number Diff line number Diff line change
Expand Up @@ -277,16 +277,17 @@ data, _ = paramscan(
data[(end - 10):end, :]

# We can combine all replicates with an aggregating function, such as mean, using
# the `aggregate` function from the `DataFrames` package:
# the `groupby` and `combine` functions from the `DataFrames` package:

using DataFrames: aggregate, Not, select!
using DataFrames: groupby, combine, Not, select!
using Statistics: mean
data_mean = aggregate(data, [:step, :min_to_be_happy, :numagents], mean)
gd = groupby(data,[:step, :min_to_be_happy, :numagents])
data_mean = combine(gd,[:happyperc_mood,:replicate] .=> mean)

select!(data_mean, Not(:replicate_mean))

# Note that the second argument takes the column names on which to split the data,
# i.e., it denotes which columns should not be aggregated. It should include
# the `:step` column and any parameter that changes among simulations. But it should
# not include the `:replicate` column.
# So in principle what we are doing here is simply averaging our result across the replicates.

38 changes: 37 additions & 1 deletion src/core/continuous_space.jl
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,19 @@ function move_agent!(agent::A, model::ABM{A, S, F, P}, dt = 1.0) where {A<:Abstr
return agent.pos
end

"""
move_agent!(agent::A, model::ABM{A, ContinuousSpace}, vel::NTuple{D, N}, dt = 1.0)
Propagate the agent forwards one step according to `vel` and the model's space, with `dt` as the time step. (`update_vel!` is not used)
"""
function move_agent!(agent::A, model::ABM{A,S,F,P}, vel::NTuple{D, N}, dt = 1.0) where {A <: AbstractAgent, S <: ContinuousSpace, F, P, D, N <: AbstractFloat}
agent.pos = agent.pos .+ dt .* vel
if model.space.periodic
agent.pos = mod.(agent.pos, model.space.extend)
end
update_space!(model, agent)
return agent.pos
end

"""
update_space!(model::ABM{A, ContinuousSpace}, agent)
Update the internal representation of continuous space to match the new position of
Expand Down Expand Up @@ -352,16 +365,24 @@ The argument `method` provides three pairing scenarios
agent is paired to its nearest neighbor. Similar to `:nearest`, each agent can belong
to only one pair. This functionality is useful e.g. when you want some agents to be
paired "guaranteed", even if some other agents might be nearest to each other.
- `:types`: For mixed agent models only. Return every pair of agents within radius `r`
(similar to `:all`), only capturing pairs of differing types. For example, a model of
`Union{Sheep,Wolf}` will only return pairs of `(Sheep, Wolf)`. In the case of multiple
agent types, *e.g.* `Union{Sheep, Wolf, Grass}`, skipping pairings that involve
`Grass`, can be achived by a [`scheduler`](@ref Schedulers) that doesn't schedule `Grass`
types, *i.e.*: `scheduler = [a.id for a in allagents(model) of !(a isa Grass)]`.
"""
function interacting_pairs(model::ABM, r::Real, method; scheduler = model.scheduler)
@assert method (:scheduler, :nearest, :all)
@assert method (:scheduler, :nearest, :all, :types)
pairs = Tuple{Int,Int}[]
if method == :nearest
true_pairs!(pairs, model, r)
elseif method == :scheduler
scheduler_pairs!(pairs, model, r, scheduler)
elseif method == :all
all_pairs!(pairs, model, r)
elseif method == :types
type_pairs!(pairs, model, r, scheduler)
end
return PairIterator(pairs, model.agents)
end
Expand Down Expand Up @@ -416,6 +437,21 @@ function true_pairs!(pairs::Vector{Tuple{Int,Int}}, model::ABM, r::Real)
end
end

function type_pairs!(pairs::Vector{Tuple{Int,Int}}, model::ABM, r::Real, scheduler)
# We don't know ahead of time what types the scheduler will provide. Get a list.
available_types = unique(typeof(model[id]) for id in scheduler(model))
for id in scheduler(model)
for nid in space_neighbors(model[id], model, r)
neigbor_type = typeof(model[nid])
if neigbor_type available_types && neigbor_type !== typeof(model[id])
# Sort the pair to overcome any uniqueness issues
new_pair = isless(id, nid) ? (id, nid) : (nid, id)
new_pair pairs && push!(pairs, new_pair)
end
end
end
end

function pair_distance(pos1, pos2, metric::Symbol)
if metric == :euclidean
sqrt(sum(abs2.(pos1 .- pos2)))
Expand Down
6 changes: 4 additions & 2 deletions test/api_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,11 @@ end

# ContinuousSpace
model = ABM(Agent6, ContinuousSpace(2))
agent = add_agent!((0.0,0.0), model, (0.5,0.0), 1.0)
agent = add_agent!((0.0, 0.0), model, (0.5, 0.0), 1.0)
move_agent!(agent, model)
@test agent.pos == (0.5,0.0)
@test agent.pos == (0.5, 0.0)
move_agent!(agent, model, (0.1, 0.5))
@test agent.pos == (0.6, 0.5)
end

@testset "kill_agent!" begin
Expand Down
22 changes: 11 additions & 11 deletions test/collect_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ model = initialize()
end

@testset "aggname" begin
@test aggname(:weight, mean) == :mean_weight
@test aggname(x_position, length) == :length_x_position
@test aggname(:weight, mean) == "mean_weight"
@test aggname(x_position, length) == "length_x_position"
end

@testset "Aggregate Collections" begin
Expand All @@ -54,7 +54,7 @@ end
collect_agent_data!(df, model, props, 1)
# Expecting weight values of all three agents. ID and step included.
@test size(df) == (3, 3)
@test names(df) == [:step, :id, :weight]
@test propertynames(df) == [:step, :id, :weight]
@test mean(df[!, :weight]) 0.3917615139

props = [(:weight, mean)]
Expand All @@ -63,22 +63,22 @@ end
# Activate aggregation. Weight column is expected to be one value for this step,
# renamed mean(weight). ID is meaningless and will therefore be dropped.
@test size(df) == (1, 2)
@test names(df) == [:step, :mean_weight]
@test propertynames(df) == [:step, :mean_weight]
@test df[1, aggname(:weight, mean)] 0.3917615139

# Add a function as a property
props = [:weight, x_position]
df = init_agent_dataframe(model, props)
collect_agent_data!(df, model, props, 1)
@test size(df) == (3, 4)
@test names(df) == [:step, :id, :weight, :x_position]
@test propertynames(df) == [:step, :id, :weight, :x_position]
@test mean(df[!, :x_position]) 4.3333333

props = [(:weight, mean), (x_position, mean)]
df = init_agent_dataframe(model, props)
collect_agent_data!(df, model, props, 1)
@test size(df) == (1, 3)
@test names(df) == [:step, :mean_weight, :mean_x_position]
@test propertynames(df) == [:step, :mean_weight, :mean_x_position]
@test df[1, aggname(x_position, mean)] 4.3333333
end

Expand All @@ -99,11 +99,11 @@ end
)

@test size(agent_data) == (11, 2)
@test names(agent_data) == [:step, :mean_weight]
@test propertynames(agent_data) == [:step, :mean_weight]
@test maximum(agent_data[!, :step]) == 1820

@test size(model_data) == (6, 3)
@test names(model_data) == [:step, :flag, :year]
@test propertynames(model_data) == [:step, :flag, :year]
@test maximum(model_data[!, :step]) == 1825

agent_data, model_data = run!(
Expand Down Expand Up @@ -143,15 +143,15 @@ end
end

@test size(daily_model_data) == (1825, 3)
@test names(daily_model_data) == [:step, :flag, :year]
@test propertynames(daily_model_data) == [:step, :flag, :year]
@test maximum(daily_model_data[!, :step]) == 1825

@test size(daily_agent_aggregate) == (1825, 2)
@test names(daily_agent_aggregate) == [:step, :mean_weight]
@test propertynames(daily_agent_aggregate) == [:step, :mean_weight]
@test maximum(daily_agent_aggregate[!, :step]) == 1825

@test size(yearly_agent_data) == (15, 3)
@test names(yearly_agent_data) == [:step, :id, :weight]
@test propertynames(yearly_agent_data) == [:step, :id, :weight]
@test maximum(yearly_agent_data[!, :step]) == 5

@test dummystep(model) == nothing
Expand Down
42 changes: 42 additions & 0 deletions test/continuousSpace_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,22 @@
@test 4 < length(space_neighbors((1, 1), e, r)) < 10
end

mutable struct AgentU1 <: AbstractAgent
id::Int
pos::NTuple{2,Float64}
vel::NTuple{2,Float64}
end

mutable struct AgentU2 <: AbstractAgent
id::Int
pos::NTuple{2,Float64}
vel::NTuple{2,Float64}
end

function ignore_six(model::ABM{A,S,F,P}) where {A,S,F,P}
[a.id for a in allagents(model) if !(typeof(a) <: Agent6)]
end

@testset "Interacting pairs" begin
space = ContinuousSpace(2, extend = (10, 10), periodic = false, metric = :euclidean)
model = ABM(Agent6, space; scheduler = fastest)
Expand Down Expand Up @@ -125,4 +141,30 @@ end
pairs = interacting_pairs(model2, 2.0, :all).pairs
@test length(pairs) == 5
@test (1, 4) pairs

Random.seed!(658)
space3 = ContinuousSpace(2, extend = (1, 1), periodic = false, metric = :euclidean)
model3 = ABM(Union{Agent6, AgentU1, AgentU2}, space3; warn = false)
for i in 1:10
add_agent!(Agent6(i, (rand(), rand()), (0.0, 0.0), 0), model3)
end
for i in 11:20
add_agent!(AgentU1(i, (rand(), rand()), (0.0, 0.0)), model3)
end
for i in 21:30
add_agent!(AgentU2(i, (rand(), rand()), (0.0, 0.0)), model3)
end
pairs = interacting_pairs(model3, 0.1, :types).pairs
@test length(pairs) == 11
for (a,b) in pairs
@test typeof(model3[a]) !== typeof(model3[b])
end
@test (3, 6) pairs

# Test that we have at least some Agent6's in this match
@test any(typeof(model3[a]) <: Agent6 || typeof(model3[b]) <: Agent6 for (a,b) in pairs)
pairs = interacting_pairs(model3, 0.1, :types; scheduler = ignore_six).pairs
@test length(pairs) == 3
# No Agent6's when using the ignore_six scheduler
@test all(!(typeof(model3[a]) <: Agent6) && !(typeof(model3[b]) <: Agent6) for (a,b) in pairs)
end

2 comments on commit ac5b93a

@Datseris
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register()

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/16215

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v3.1.1 -m "<description of version>" ac5b93aa7d60e912c120ee9ba9fba8e79da630c7
git push origin v3.1.1

Please sign in to comment.