-
Notifications
You must be signed in to change notification settings - Fork 118
/
schoolyard.jl
165 lines (136 loc) · 6.61 KB
/
schoolyard.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# # Social networks with Graphs.jl
# ```@raw html
# <video width="auto" controls autoplay loop>
# <source src="../schoolyard.mp4" type="video/mp4">
# </video>
# ```
# Many ABM frameworks provide graph infrastructure for analysing network properties of agents.
# Agents.jl is no different in that aspect, we have [`GraphSpace`](@ref) for when spatial structure
# is not important, but connections are.
# What if you wish to model something a little more complex? Perhaps a school yard full of students
# running around (in space), interacting via some social network. This is precisely the scenario that
# the [MASON](https://cs.gmu.edu/~eclab/projects/mason/) ABM framework uses as an introductory example
# in their [documentation](https://cs.gmu.edu/~eclab/projects/mason/manual.pdf).
# Rather than implementing an Agents.jl⸺specific graph structure, we can interface with
# [Graphs.jl](https://github.com/JuliaGraphs/Graphs.jl): a high class library for managing
# and implementing graphs, which can be re-used to establish social networks within existing spaces.
# To begin, we load in some dependencies
using Agents
using SimpleWeightedGraphs: SimpleWeightedDiGraph # will make social network
using SparseArrays: findnz # for social network connections
using Random: MersenneTwister # reproducibility
# And create an alias to `ContinuousAgent{2,Float64}`,
# as our agents don't need additional properties.
const Student = ContinuousAgent{2,Float64}
# ## Rules of the schoolyard
# It's lunchtime, and the students are going out to play.
# We assume the school building is in the centre of our space, with some fences around the building.
# A teacher monitors the students, and makes sure they don't stray too far towards the fence.
# We use a `teacher_attractor` force to simulate a teacher's attentiveness.
# Students head out to the schoolyard in random directions, but adhere to some social norms.
# Each student has one *friend* and one *foe*. These are chosen at random in our model, so it's
# possible that for any pair of students, one likes the other but this feeling is not reciprocated.
# The bond between pairs is chosen at random between 0 and 1, with a bond of 1 being the strongest.
# If the bond is *friendly*, agents wish above all else to be near their *friend*.
# Bonds that are *unfriendly* see students moving as far away as possible from their *foe*.
# ## Initialising the model
function schoolyard(;
numStudents = 50,
teacher_attractor = 0.15,
noise = 0.1,
max_force = 1.7,
spacing = 4.0,
seed = 6998,
velocity = (0, 0),
)
model = StandardABM(
Student,
ContinuousSpace((100, 100); spacing=spacing, periodic=false);
agent_step!,
properties = Dict(
:teacher_attractor => teacher_attractor,
:noise => noise,
:buddies => SimpleWeightedDiGraph(numStudents),
:max_force => max_force,
),
rng = MersenneTwister(seed)
)
for student in 1:numStudents
## Students begin near the school building
position = abmspace(model).extent .* 0.5 .+ rand(abmrng(model), SVector{2}) .- 0.5
add_agent!(position, model, velocity)
## Add one friend and one foe to the social network
friend = rand(abmrng(model), filter(s -> s != student, 1:numStudents))
add_edge!(model.buddies, student, friend, rand(abmrng(model)))
foe = rand(abmrng(model), filter(s -> s != student, 1:numStudents))
add_edge!(model.buddies, student, foe, -rand(abmrng(model)))
end
model
end
# Our model contains the `buddies` property, which is our Graphs.jl directed, weighted graph.
# As we can see in the loop, we choose one `friend` and one `foe` at random for each `student` and
# assign their relationship as a weighted edge on the graph.
# ## Movement dynamics
distance(pos) = sqrt(pos[1]^2 + pos[2]^2)
scale(L, force) = (L / distance(force)) .* force
function agent_step!(student, model)
## place a teacher in the center of the yard, so we don’t go too far away
teacher = (abmspace(model).extent .* 0.5 .- student.pos) .* model.teacher_attractor
## add a bit of randomness
noise = model.noise .* (rand(abmrng(model), SVector{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 = scale(model.max_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 = scale(L, force)
end
end
network_force = network_force .+ force
end
## Add all forces together to assign the students next position
new_pos = student.pos .+ noise .+ teacher .+ network_force
move_agent!(student, new_pos, model)
end
# Applying the rules for movement is relatively simple. For the network specifically,
# we find the student's `network` and figure out how far apart they are. We scale this
# by the `buddiness` factor (how much force we should apply), then figure out if
# that force should be in a positive or negative direction (*friend* or *foe*?).
# The `findnz` function is something that may require some further explanation.
# Graphs uses sparse vectors internally to efficiently represent data.
# When we find the `network` of our `student`, we want to convert the result to
# a dense representation by **find**ing the **n**on-**z**ero (`findnz`) elements.
model = schoolyard()
# ## Visualising the system
# Now, we can watch the dynamics of the social system unfold:
using CairoMakie
CairoMakie.activate!() # hide
const ABMPlot = Agents.get_ABMPlot_type()
function Agents.static_preplot!(ax::Axis, p::ABMPlot)
obj = CairoMakie.scatter!([50 50]; color = :red) # Show position of teacher
CairoMakie.hidedecorations!(ax) # hide tick labels etc.
CairoMakie.translate!(obj, 0, 0, 5) # be sure that the teacher will be above students
end
abmvideo(
"schoolyard.mp4", model;
framerate = 15, frames = 40,
title = "Playgound dynamics",
)
# ```@raw html
# <video width="auto" controls autoplay loop>
# <source src="../schoolyard.mp4" type="video/mp4">
# </video>
# ```