Agents.jl: Designing mixed spaces

Created on 25 Sep 2020  路  13Comments  路  Source: JuliaDynamics/Agents.jl

As far as infrastructure goes, it is not hard to combine 2 spaces. We can define a MixedSpace struct, and because of our recent space source code rework it is easy to extend move, add, kill to simply call the functions for each of the underlying space.
In my understanding, a mixed space makes sense only if the second space is a graph...?

What needs discussion:

  1. What should move_agent_pos do? Should it dispatch depending on the position type? i.e. if it is integer it moves the agent to node i, otherwise to position i ?
  2. When we do "add agent" where do we put the agent in the graph? do we extend add_agent_pos to take a forth argument, the graph node?
  3. How do we modify the nearby functions to be usable in both graph and real space?

@Libbum in #265 you said that "it is very standard" for MASON/NetLogo, https://github.com/JuliaDynamics/Agents.jl/issues/265#issuecomment-698141944. Yet, while opening this issue, I had difficulty even coming up with what are the correct questions to ask, much less so what the implementation should be.

Perhaps you can write here what _exactly_ do MASON and NetLogo mean by having "two spaces" and how they treat "finding nearest neighbors" and "where agents are"?

discussion

All 13 comments

I'm going to psedo-code / translate to Agents.jl where I can here.

MASON

numStudents = 50
yard = ContinuousSpace(2, extend = (100, 100))
model = ABM(Student, yard)
for _ in 1:numStudents
    add_agent!(model) # More specifically, they add students randomly around (50, 50) 卤 0.5
end

A teacher sits in the center of the yard, students move randomly about the yard, but have a force vector that attracts them to the teacher and compels them to stay away from the fence. This means they mostly huddle around the teacher (depending on the strength of the teachers attractor).

Now a social network is added.

buddies = GraphSpace(path_graph(numStudents))

Which is added to the Students class, our equivalent would be something like ABM(Student, yard, buddies) or a space union perhaps.

The initialisation would be extended to something like this:

numStudents = 50
yard = ContinuousSpace(2, extend = (100, 100))
buddies = GraphSpace(path_graph(numStudents))
model = ABM(Student, yard, buddies)
for _ in 1:numStudents
    s = add_agent!(model, yard) # Not true syntax, just an example
    add_agent!(s, model, buddies) # to illustrate the mechanics of MASON
end
for student in 1:numStudents
    friend = rand(filter(s->s!=student, 1:numStudents))
    add_edge!(model.buddies, student, friend, rand())
    foe = rand(filter(s->s!=student, 1:numStudents))
    add_edge!(model.buddies, student, foe, -rand())
end

So now the social network is an undirected graph, with weighted edges. Each student as a friend, with a strength between [0-1] and a foe with the same range. A pair can both like and dislike each other.

Now, the movement routine is altered such that there's the safety-from-the-fence, towards-the-teacher force, but also a self organising force based on the network. Friends want to stay closer to each other, foes want to stay apart.

Netlogo

Has a slightly different philosophy and I can't say exactly how this is implemented, just how it's described.

You have patch agents that don't move, but have property on a GridSpace (think Grass in wolf-sheep or Temperature in daisyworld). Turtle agents exist only within ContinuousSpace (i.e. they have a Tuple{Float64,Foat64} position) - but since Netlogo's Continuous space is actually a CompartmentSpace, we can understand this agent type as our normal agent type on a continous space. The last relevant agent type is a link agent, which is a connection between two turtles. If one turtle dies, the link goes with it.

Links are graphs. They have all the same primitives. You can take a look at some particulars in the documentation.

A simple example:

;; every link breed must be declared as either directed or undirected
directed-link-breed [red-links red-link]
undirected-link-breed [blue-links blue-link]

blue-links-own [ weight ]  ;; link breeds can own variables just like turtle breeds

to setup
  clear-all
  create-ordered-turtles 10 [
    fd 5
    set color gray
  ]
  ask n-of 5 turtles [
    ;; create-<breed>-with is used to make undirected links
    create-blue-link-with one-of other turtles [
      set color blue
      set weight random 10
      set label weight
    ]
  ]
  ;; different breeds can have different default shapes
  set-default-shape red-links "curved link"
  ask n-of 5 turtles [
    ;; create-<breed>-to/from are used to make directed links
    create-red-link-to one-of other turtles [
      set color red
    ]
  ]
  reset-ticks
end

This adds 10 turtle agents to preset positions on the grid, then adds on top of that, two link networks—one directed graph and one undirected graph with some weighting.

For us to implement this in Agents.jl we would require a CompartmentSpace and two GridSpaces.

Perhaps you can write here what exactly do MASON and NetLogo mean by having "two spaces" and how they treat "finding nearest neighbors" and "where agents are"?

Both MASON and Netlogo do this as you would expect in these examples.

The students: where they are is in the yard (gridspace), and they force vectors for there movement depend on the buddies (standard NN practices on the graph).

Similar with Netlogo: if you want to move someone, depending on whether you mean move their 'real world' position or their 'graph property', you need to adjust each space individually.

We could leave this be, and just do something like this:

model = ABM(Student, ContinuousSpace(2); properties = Dict(:buddies => GraphSpace(path_graph(50)))

We _could_ leave this be, and just do something like this:

model = ABM(Student, ContinuousSpace(2); properties = Dict(:buddies => GraphSpace(path_graph(50)))

Do you see any disadvantage in keeping it this?

A possible disadvantage in implementing mixed spaces is losing the API simplicity.

I must say, I have completely misinterpreted the situation. I guess I shouldn't take the comment of @pszufe of how this is a "second space" too literally.

I can now clearly see that the example Tim presents here, with the schoolyard and the social network, is not a case of two spaces coexisting. In Agents.jl a GraphSpace is not a social network, it a special graph that any node of the graph can hold an arbitrary amount of agents and agents can move between nodes.

A social network is something much simpler than a GraphSpace. It is just a standard _and mutable_ graph from LightGraphs.jl that makes each agent ID to be a node. Exactly because of this bijection property there is no reason for agents to have "a position type for the social netwokr", as their ID coincides with this position, and it is also impossible to change this position.

As both Tim and Ali already understood and said here, what MASON does is already possible, and in fact trivial, within Agents.jl by making a graph a property.

I would vote to do the following:

  1. Close this issue, forget the "mixed space discussion".
  2. Create a schoolyard example which showcases how to implement a social network on top of existing space. This is so much more in spirit of Julia, where instead of we re-inventing the wheel and doing some "special" implementation, we are simply saying "use the existing library that does the thing you want to do".
  3. In the doc example with the schoolyard, we can say something like "Julia already has a high class library for managing and implementing graphs, called LightGraphs.jl , which can be re-used to establish social networks within existing spaces.".
  4. (only remotely related to this discussion): consider solving https://github.com/JuliaDynamics/Agents.jl/issues/264 , which I got reminded of when I mentioned that the social network must be a mutable graph.
using Agents, LightGraphs, SimpleWeightedGraphs, SparseArrays, LinearAlgebra, AgentsPlots

mutable struct Student <: AbstractAgent
    id::Int
    pos::Tuple{Float64,Float64}
end

numStudents = 50

model = ABM(
    Student,
    ContinuousSpace(2; extend = (100, 100));
    properties = Dict(
        :teacher_attractor => 0.2,
        :noise => 0.1,
        :buddies => SimpleWeightedGraph(numStudents),
        :max_force => 1.0,
    ),
)
for student in 1:numStudents
    add_agent!(model.space.extend .* 0.5 .+ Tuple(rand(2)) .- 0.5, model)

    friend = rand(filter(s->s!=student, 1:numStudents))
    add_edge!(model.buddies, student, friend, rand())
    foe = rand(filter(s->s!=student, 1:numStudents))
    add_edge!(model.buddies, student, foe, -rand())
end

distance(a) = sqrt(a[1]^2+a[2]^2)

function agent_step!(student, model)
    # add in a vector to the "teacher" -- the center of the yard, so we don鈥檛 go too far away
    teacher = (model.space.extend .* 0.5 .- student.pos) .* model.teacher_attractor

    # add a bit of randomness
    noise = model.noise .* (Tuple(rand(2)) .- 0.5)

    # Adhere to the social network
    network = model.buddies.weights[student.id,:]
    tidxs, tweights = findnz(network)
    network_force = (0.0, 0.0)
    for (widx, tidx) in enumerate(tidxs)
        buddiness = tweights[widx]
        force = (student.pos .- model[tidx].pos) .* buddiness
        if buddiness >= 0
            # The further I am from them, the more I want to go to them
            if distance(force) > model.max_force # I'm far enough away
                force = (model.max_force/norm(force)).*force
            end
        else
            # The further I am away from them, the better
            if distance(force) > model.max_force # I'm far enough away
                force = (0.0, 0.0)
            else
                L = model.max_force - distance(force)
                force = (L/norm(force)).*force
            end
        end
        network_force = network_force .+ force
    end

    student.pos = student.pos .+ noise .+ teacher .+ network_force
    update_space!(model, student)
end

anim = @animate for i in 0:20
    i > 0 && step!(model, agent_step!, 1)
    p1 = plotabm(
        model;
        xlims = (0, 100),
        ylims = (0, 100),
    )
    title!(p1, "step $(i)")
end
gif(anim, "play.gif", fps = 20)

play

I agree with all your points above George. It was good to get into this and make sure we didn't re-invent anything & also make an informed choice about how to move forward.

Indeed, when building this example, it was super obvious this was the best way to get it done. I'll write up a better example in a PR.

Should this be an example or an "Ecosystem Integration"?

Sure, "Ecosystem integration" seems more fitting, you are right.

4\. consider solving #264 , which I got reminded of when I mentioned that the social network must be a mutable graph.

I see #264 to be solvable with using the properties. From what understood, the OP assigns each graph vertex to single individuals. I would use no space for such a scenario and have a graph describing the relationship between individuals that get updated too.

You are right, the user in #264 indeed has a "social network only" kind of graph space. However in general I think it is worth it to allow our current graphspace to allow adding and deleting nodes. Imagine modelling an epidemic in the city and nodes are stores and homes. Some stores close down and you could model this by (a) ensuring in your code that agents don't go to that store or (b) deleting the store node all together. I think (b) is a simpler way to do this.

Yes, a mutable graph is can be useful, if it doesn't sacrifice speed.

I'll do a pr on that.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Libbum picture Libbum  路  5Comments

pitmonticone picture pitmonticone  路  8Comments

drdozer picture drdozer  路  5Comments

fbanning picture fbanning  路  10Comments

Datseris picture Datseris  路  9Comments