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:
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 ?add_agent_pos to take a forth argument, the graph node?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"?
I'm going to psedo-code / translate to Agents.jl where I can here.
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.
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:
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)

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.