Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow using NoSpaceAgent in spatial models #720

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
10 changes: 8 additions & 2 deletions src/core/agents.jl
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ that the resulting new agent type will have.
The `id` is an unchangable field (and in Julia versions ≥ v1.8 this is enforced).
Use functions like [`move_agent!`](@ref) etc., to change the position.

You can use the `@doc` macro from Julia to document the generated struct if you wish so.

## Examples
### Example without optional hierarchy
Using
Expand Down Expand Up @@ -197,8 +195,13 @@ macro agent(new_name, base_type, extra_fields)
# It is important to evaluate the macro in the module that it was called at
Core.eval($(__module__), expr)
end
# allow attaching docstrings to the new struct, issue #715
Core.@__doc__($(esc(Docs.namify(new_name))))
nothing
end
end


# TODO: I do not know how to merge these two macros to remove code duplication.
# There should be away that only the 4-argument version is used
# and the 3-argument version just passes `AbstractAgent` to the 4-argument.
Expand Down Expand Up @@ -234,6 +237,9 @@ macro agent(new_name, base_type, super_type, extra_fields)
# It is important to evaluate the macro in the module that it was called at
Core.eval($(__module__), expr)
end
# allow attaching docstrings to the new struct, issue #715
Core.@__doc__($(esc(Docs.namify(new_name))))
nothing
end
end

Expand Down
32 changes: 16 additions & 16 deletions src/core/model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ function agent_validator(
seeing this warning because you gave `Agent` instead of `Agent{Float64}`
(for example) to this function. You can also create an instance of your agent
and pass it to this function. If you want to use `Union` types for mixed agent
models, you can silence this warning.
models, you can silence this warning by passing `warn=false` to `AgentBasedModel()`.
"""
for type in union_types(A)
do_checks(type, space, warn)
Expand All @@ -323,24 +323,24 @@ function do_checks(::Type{A}, space::S, warn::Bool) where {A<:AbstractAgent,S<:S
fieldtype(A, :id) <: Integer ||
throw(ArgumentError("`id` field in Agent struct must be of type `Int`."))
if space !== nothing
(any(isequal(:pos), fieldnames(A)) && fieldnames(A)[2] == :pos) ||
throw(ArgumentError("Second field of Agent struct must be `pos` when using a space."))
# Check `pos` field in A has the correct type
pos_type = fieldtype(A, :pos)
space_type = typeof(space)
if space_type <: GraphSpace && !(pos_type <: Integer)
throw(ArgumentError("`pos` field in Agent struct must be of type `Int` when using GraphSpace."))
elseif space_type <: GridSpace && !(pos_type <: NTuple{D,Integer} where {D})
throw(ArgumentError("`pos` field in Agent struct must be of type `NTuple{Int}` when using GridSpace."))
elseif space_type <: ContinuousSpace || space_type <: ContinuousSpace
if !(pos_type <: NTuple{D,<:AbstractFloat} where {D})
if warn && !(any(isequal(:pos), fieldnames(A)) && fieldnames(A)[2] == :pos)
@warn "Second field of Agent struct must be `pos` when using a space, unless you are purposely working with a NoSpaceAgent."
elseif any(isequal(:pos), fieldnames(A))
# Check `pos` field in A has the correct type
pos_type = fieldtype(A, :pos)
if space_type <: GraphSpace && !(pos_type <: Integer)
throw(ArgumentError("`pos` field in Agent struct must be of type `Int` when using GraphSpace."))
elseif space_type <: GridSpace && !(pos_type <: NTuple{D,Integer} where {D})
throw(ArgumentError("`pos` field in Agent struct must be of type `NTuple{Int}` when using GridSpace."))
elseif space_type <: ContinuousSpace && !(pos_type <: NTuple{D,<:AbstractFloat} where {D})
throw(ArgumentError("`pos` field in Agent struct must be of type `NTuple{<:AbstractFloat}` when using ContinuousSpace."))
end
if warn &&
any(isequal(:vel), fieldnames(A)) &&
!(fieldtype(A, :vel) <: NTuple{D,<:AbstractFloat} where {D})
@warn "`vel` field in Agent struct should be of type `NTuple{<:AbstractFloat}` when using ContinuousSpace."
end
end
if warn && space_type <: ContinuousSpace &&
any(isequal(:vel), fieldnames(A)) &&
!(fieldtype(A, :vel) <: NTuple{D,<:AbstractFloat} where {D})
@warn "`vel` field in Agent struct should be of type `NTuple{<:AbstractFloat}` when using ContinuousSpace."
end
end
end
Expand Down
9 changes: 7 additions & 2 deletions src/core/space_interaction_API.jl
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,13 @@ function add_agent!(
kwargs...,
)
id = nextid(model)
newagent = A(id, pos, properties...; kwargs...)
add_agent_pos!(newagent, model)
if any(isequal(:pos), fieldnames(A))
newagent = A(id, pos, properties...; kwargs...)
add_agent_pos!(newagent, model)
Comment on lines +288 to +290
Copy link
Member

Choose a reason for hiding this comment

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

Wait, isn't this a hit to performance? Please check if there is a more efficient way to test if a struct as a field pos.

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if any(isequal(:pos), fieldnames(A))
newagent = A(id, pos, properties...; kwargs...)
add_agent_pos!(newagent, model)
if :pos in fieldnames(A)
newagent = A(id, pos, properties...; kwargs...)
add_agent_pos!(newagent, model)

I believe this is the idiomatic way of doing this.

else #if adding a NoSpaceAgent to a spaced model
newagent = A(id, properties...; kwargs...)
model[id] = newagent
end
end

#######################################################################################
Expand Down
13 changes: 6 additions & 7 deletions src/spaces/continuous.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,16 @@ function Base.show(io::IO, space::ContinuousSpace{D,P}) where {D,P}
print(io, s)
end

@agent ContinuousAgent{D} NoSpaceAgent begin
pos::NTuple{D,Float64}
vel::NTuple{D,Float64}
end

@doc """
"""
ContinuousAgent{D} <: AbstractAgent
The minimal agent struct for usage with `D`-dimensional [`ContinuousSpace`](@ref).
It has the additoinal fields `pos::NTuple{D,Float64}, vel::NTuple{D,Float64}`.
See also [`@agent`](@ref).
""" ContinuousAgent
"""
@agent ContinuousAgent{D} NoSpaceAgent begin
pos::NTuple{D,Float64}
vel::NTuple{D,Float64}
end

"""
ContinuousSpace(extent::NTuple{D, <:Real}; kwargs...)
Expand Down
13 changes: 6 additions & 7 deletions src/spaces/graph.jl
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,14 @@ function Base.show(io::IO, s::GraphSpace)
print(io, "GraphSpace with $(nv(s.graph)) positions and $(ne(s.graph)) edges")
end

@agent GraphAgent NoSpaceAgent begin
pos::Int
end

@doc """
"""
GraphAgent <: AbstractAgent
The minimal agent struct for usage with [`GraphSpace`](@ref).
It has an additional `pos::Int` field. See also [`@agent`](@ref).
""" GraphAgent
"""
@agent GraphAgent NoSpaceAgent begin
pos::Int
end

#######################################################################################
# Agents.jl space API
Expand Down Expand Up @@ -205,4 +204,4 @@ Graphs.add_edge!(model::ABM{<:GraphSpace}, args...; kwargs...) = add_edge!(model
Remove an edge (relationship between two positions) from the graph.
Returns a boolean, true if the operation was successful.
"""
Graphs.rem_edge!(model::ABM{<:GraphSpace}, n, m) = rem_edge!(model.space.graph, n, m)
Graphs.rem_edge!(model::ABM{<:GraphSpace}, n, m) = rem_edge!(model.space.graph, n, m)
11 changes: 5 additions & 6 deletions src/spaces/grid_general.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@ to vector of indices within each radius.
"""
abstract type AbstractGridSpace{D,P} <: DiscreteSpace end

@agent GridAgent{D} NoSpaceAgent begin
pos::NTuple{D, Int}
end

@doc """
"""
GridAgent{D} <: AbstractAgent
The minimal agent struct for usage with `D`-dimensional [`GridSpace`](@ref).
It has an additional `pos::NTuple{D,Int}` field. See also [`@agent`](@ref).
""" GridAgent
"""
@agent GridAgent{D} NoSpaceAgent begin
pos::NTuple{D, Int}
end

function positions(space::AbstractGridSpace)
x = CartesianIndices(space.stored_ids)
Expand Down
12 changes: 6 additions & 6 deletions src/spaces/openstreetmap.jl
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,14 @@ function Base.show(io::IO, s::OpenStreetMapSpace)
)
end

@agent OSMAgent NoSpaceAgent begin
pos::Tuple{Int,Int,Float64}
end
@doc """
"""
OSMAgent <: AbstractAgent
The minimal agent struct for usage with [`OpenStreetMapSpace`](@ref).
It has an additional field `pos::Tuple{Int,Int,Float64}`. See also [`@agent`](@ref).
""" OSMAgent
"""
@agent OSMAgent NoSpaceAgent begin
pos::Tuple{Int,Int,Float64}
end

"""
OSM.test_map()
Expand Down Expand Up @@ -1011,4 +1011,4 @@ end # module OSM
# These are for aliasing the in-module names, and exporting them at top level
const OpenStreetMapSpace = OSM.OpenStreetMapSpace
const OSMSpace = OSM.OpenStreetMapSpace
const OSMAgent = OSM.OSMAgent
const OSMAgent = OSM.OSMAgent
29 changes: 20 additions & 9 deletions test/model_creation_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ using Test, Agents, Random
@test fieldnames(A3) == (:id, :pos, :weight)
@test fieldtypes(A3) == (Int, NTuple{2, Int}, Float64)

"""
This is a test docstring for agent A4
"""
@agent A4 A3 begin
z::Bool
end
@test A4 <: AbstractAgent
@test fieldnames(A4) == (:id, :pos, :weight, :z)
@test fieldtypes(A4) == (Int, NTuple{2, Int}, Float64, Bool)
@test contains(string(@doc(A4)), "This is a test docstring for agent A4")

# Also test subtyping
abstract type AbstractHuman <: AbstractAgent end
Expand Down Expand Up @@ -65,10 +69,17 @@ end
@test_throws ArgumentError ABM(BadAgentId)
agent = BadAgentId(1.0)
@test_throws ArgumentError ABM(agent)
# Cannot use NoSpaceAgent in a grid space context since it has no `pos` field
@test_throws ArgumentError ABM(NoSpaceAgent, GridSpace((1, 1)))
# Warn the user about using NoSpaceAgent in a grid space context since it has no `pos` field
@test_logs (
:warn,
"Second field of Agent struct must be `pos` when using a space, unless you are purposely working with a NoSpaceAgent."
) match_mode=:any ABM(NoSpaceAgent, GridSpace((1, 1)))
agent = NoSpaceAgent(1)
@test_throws ArgumentError ABM(agent, GridSpace((1, 1)))
@test_logs (
:warn,
"Second field of Agent struct must be `pos` when using a space, unless you are purposely working with a NoSpaceAgent."
) match_mode=:any ABM(agent, GridSpace((1, 1)))
@test_logs ABM(NoSpaceAgent, GridSpace((1, 1)), warn=false) #no warnings with warn=false
# Cannot use Gridagent in a graph space context since `pos` has an invalid type
@test_throws ArgumentError ABM(GridAgent{2}, GraphSpace(Agents.Graph(1)))
agent = GridAgent{2}(1, (1, 1))
Expand All @@ -86,12 +97,12 @@ end
@test_logs (
:warn,
"`vel` field in Agent struct should be of type `NTuple{<:AbstractFloat}` when using ContinuousSpace.",
) ABM(DiscreteVelocity, ContinuousSpace((1, 1)))
) match_mode=:any ABM(DiscreteVelocity, ContinuousSpace((1, 1)))
agent = DiscreteVelocity(1, (1, 1), (2, 3), 2.4)
@test_logs (
:warn,
"`vel` field in Agent struct should be of type `NTuple{<:AbstractFloat}` when using ContinuousSpace.",
) ABM(agent, ContinuousSpace((1, 1)))
) match_mode=:any ABM(agent, ContinuousSpace((1, 1)))
# Warning is suppressed if flag is set
@test Agents.agenttype(ABM(agent, ContinuousSpace((1, 1)); warn = false)) <: AbstractAgent
# Shouldn't use ParametricAgent since it is not a concrete type
Expand All @@ -108,14 +119,14 @@ end
seeing this warning because you gave `Agent` instead of `Agent{Float64}`
(for example) to this function. You can also create an instance of your agent
and pass it to this function. If you want to use `Union` types for mixed agent
models, you can silence this warning.\n"""
) ABM(ParametricAgent, GridSpace((1, 1)))
models, you can silence this warning by passing `warn=false` to `AgentBasedModel()`.\n"""
) match_mode=:any ABM(ParametricAgent, GridSpace((1, 1)))
# Warning is suppressed if flag is set
@test Agents.agenttype(ABM(ParametricAgent, GridSpace((1, 1)); warn = false)) <:
AbstractAgent
# ParametricAgent{Int} is the correct way to use such an agent
@test Agents.agenttype(ABM(ParametricAgent{Int}, GridSpace((1, 1)))) <: AbstractAgent
#Type inferance using an instance can help users here
#Type inference using an instance can help users here
agent = ParametricAgent(1, (1, 1), 5, "Info")
@test Agents.agenttype(ABM(agent, GridSpace((1, 1)))) <: AbstractAgent
#Mixed agents
Expand All @@ -131,7 +142,7 @@ end
seeing this warning because you gave `Agent` instead of `Agent{Float64}`
(for example) to this function. You can also create an instance of your agent
and pass it to this function. If you want to use `Union` types for mixed agent
models, you can silence this warning.\n"""
models, you can silence this warning by passing `warn=false` to `AgentBasedModel()`.\n"""
) ABM(Union{NoSpaceAgent,ValidAgent})
@test_throws ArgumentError ABM(Union{NoSpaceAgent,BadAgent}; warn = false)
end