How to simulate friends on a GridSpace in Agents.jl

I am using Agents.jl and I want to create for each agent a list of (the ids) other agents that are his friends. I am working on a GridSpace (not in a GraphSpace).
For each agent, the list of friends has a random size between [a,b] and age between [ agent.age - c, agent.age + d].
I have tried the following:

mutable struct Person <: AbstractAgent
    id::Int
    pos::NTuple{2, Int}
    age::Int
    friends::Array ## list of ids of other agents
end

but when initializing the model, I should have the ages of all agents before defining the friends of each one. How can I do that? Using a sample for each agent?

So if I understood you correctly, each agent should have a list of friends with a length between a and b and the age of the friends inside that list should be between agent.age - c and agent.age - d.
There are definitely multiple ways to do it. Hm, I think I would do the following:

using Random
using Pipe

mutable struct Person <: AbstractAgent
    id::Int
    pos::NTuple{2, Int}
    age::Int
    friends::Vector{Int} # use vector of integers because typeof(agent.id) == Int
end

# inside the model initialisation function:
for n in 1:numagents
    agent = Person(
        n, # id
        (1, 1),  # position
        rand(model.rng, 0:80), # random age between 0 and 80
        fill(n, rand(model.rng, a:b)) # placeholder vector of friends with length between a and b
    )
    add_agent!(agent, model) # place agents at a random position on the grid
end

for agent in allagents(model)
    # first retrieve a list of possible friends in the correct age range
    possible_friends = @pipe collect(allids(model)) |> # collect all existing ids
        filter!(id -> id != agent.id, _) |>  # remove self from id list
        filter!(id -> agent.age - c < model[id].age < agent.age + d, _) |> # remove ids of agents that are not in the correct age range
        shuffle!(model.rng, _) # shuffle the list to randomise the draw
    # then replace values in agent.friends with a random subset from possible_friends
    agent.friends .= possible_friends[begin:length(agent.friends)] # 
end

Couldn’t test it because I don’t have your full model but hopefully you got some ideas out of my write-up. It’s probably not the most performant way to do things but it should work. :slight_smile:

I’ve also tried to be a bit liberal with the comments, since I don’t know about your current level of coding skills. If you didn’t understand something, feel free to ask for more info.

1 Like

Thank you for your great help. I am an untalented amateur in coding but I understood almost all your code, without reading your helpful comments (except for the fill line :slight_smile: ).
I tried to run your code in my (almost) complete model but I got a bounds error that I must debug.
I have changed the age bounds and also gave values to the variables a, b, c, d.
I am sending now the complete code and error message:

using Agents
using Random
using Pipe

mutable struct Person <: AbstractAgent
    id::Int
    pos::NTuple{2, Int}
    age::Int
    friends::Vector{Int} # use vector of integers because typeof(agent.id) == Int
end

function initialize(; numagents = 81, griddims = (81, 100), seed = 125)
    space = GridSpace(griddims, periodic = false)
    rng = Random.MersenneTwister(seed)
    model = ABM(
        Person, space;
        rng, scheduler = Schedulers.randomly
    )
a = 10; b = 25; c = 5; d = 5
# inside the model initialisation function:
for n in 1:numagents
    agent = Person(
        n, # id
        (1, 1),  # position
        rand(model.rng, 20:100), # random age between 20 and 100
        fill(n, rand(model.rng, a:b)) # placeholder vector of friends with length between a and b
    )
    add_agent!(agent, model) # place agents at a random position on the grid
end

for agent in allagents(model)
    # first retrieve a list of possible friends in the correct age range
    possible_friends = @pipe collect(allids(model)) |> # collect all existing ids
        filter!(id -> id != agent.id, _) |>  # remove self from id list
        filter!(id -> agent.age - c < model[id].age < agent.age + d, _) |> # remove ids of agents that are not in the correct age range
        shuffle!(model.rng, _) # shuffle the list to randomise the draw
    # then replace values in agent.friends with a random subset from possible_friends
    agent.friends .= possible_friends[begin:length(agent.friends)] # 
end

return model
end

model = initialize()

The error message is:

ERROR: LoadError: BoundsError: attempt to access 7-element Vector{Int64} at index [1:16]
Stacktrace:
 [1] throw_boundserror(A::Vector{Int64}, I::Tuple{UnitRange{Int64}})
   @ Base .\abstractarray.jl:691
 [2] checkbounds
   @ .\abstractarray.jl:656 [inlined]
 [3] getindex
   @ .\array.jl:867 [inlined]
 [4] initialize(; numagents::Int64, griddims::Tuple{Int64, Int64}, seed::Int64)
   @ Main c:\Users\sbac\Dropbox\JULIA\coordTransSocNets\disc.jl:38
 [5] initialize()
   @ Main c:\Users\sbac\Dropbox\JULIA\coordTransSocNets\disc.jl:13
 [6] top-level scope
   @ c:\Users\sbac\Dropbox\JULIA\coordTransSocNets\disc.jl:44
in expression starting at c:\Users\sbac\Dropbox\JULIA\coordTransSocNets\disc.jl:44

The problem most likely lies in this statement possible_friends[begin:length(agent.friends).
If your list of possible_friends is too small (e.g. only 7 ids in there) but your agent should have more friends than that (e.g. the random draw between a and b was 16), then you will encounter such a BoundsError.
So what you’re facing here is the fact that you don’t know about the final length of the friends vector during the initialisation process (because it depends dynamically on a,b, age, c, and d). The preallocation that I’ve tried with the fill function will likely not work properly then.

Here’s how you could solve it:

# adjust the agent struct to save the maximum number of friends an agent can have
mutable struct Person <: AbstractAgent
    id::Int
    pos::NTuple{2, Int}
    age::Int
    max_friends::Int
    friends::Vector{Int}
end

# and inside the init function:
for n in 1:numagents
    agent = Person(
        n,
        (1, 1),
        rand(model.rng, 20:100),
        rand(model.rng, a:b), # random draw for maximum number of friends
        [] # initialise an empty vector of friends
    )
    add_agent!(agent, model)
end

for agent in allagents(model)
    possible_friends = @pipe collect(allids(model)) |>
        filter!(id -> id != agent.id, _) |>
        filter!(id -> agent.age - c < model[id].age < agent.age + d, _) |> 
        shuffle!(model.rng, _) # shuffle the list to randomise the draw
    for id in possible_friends
        push!(agent.friends, id) # add this id to the friends vector
        if length(agent.friends) == agent.max_friends # if friends vector has reached desired length
            break # break the for-loop
        end
    end
end
1 Like

Thank you for your help. Now the script works fine. The only issue is that max_friends is in fact number_of_friends, as I wanted.
I have yet a long way to finish my project, but I learned a lot from your code.