Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
yangliu28
GitHub Repository: yangliu28/swarm_formation_sim
Path: blob/master/trigridnet_role_assignment.py
104 views
1
# This simulation tests a distributed role assignment algorithm, for robot swarm starting
2
# at arbitrary triangle grid network, to perform one-to-one role assignment. The assigned
3
# roles are abstract and represented as index numbers. The primary goal of this role
4
# assignment algorithm is to have all nodes agree on the one-to-one assignment scheme,
5
# instead of coming up with the best assignment scheme.
6
7
# input arguments:
8
# '-f': filename of the triangle grid network
9
10
# Inter-node communication is used to let one node know the status of another node that
11
# is not directly connected. Enabling message relay is what I consider the most convenient
12
# way to resolve conflict when two nodes decide on the same assigned role. Although in this
13
# way, it is possible for each node to have the information of all the other nodes, and
14
# thus it could become a distributed master control algorithm, we still consider the proposed
15
# algorithm a distributed one. Because we limit the information each node will store, and
16
# the local computation performed, so the algorithm is still robust.
17
18
# The communication delay is also reflected in the simulation, one step in simulation will
19
# update one step of message transmission. Color is used to highlight the conflicts in the
20
# role assignment, instead of showing convergence of decisions.
21
22
# visualization design:
23
# solid black circle for undetermined role assignment scheme
24
# solid colored circle for conflicts in role assignment
25
# extra circle on node for locally converged role assignment scheme
26
27
28
import pygame
29
import matplotlib.pyplot as plt
30
from trigridnet_generator import *
31
from formation_functions import *
32
import numpy as np
33
import os, getopt, sys, time, random
34
35
import pandas as pd
36
pd.set_option('display.max_columns', None)
37
# print pd.DataFrame(gradients, range(net_size), range(net_size))
38
39
net_folder = 'trigrid-networks'
40
net_filename = '30-1' # default network
41
net_size = 30 # default network size
42
net_filepath = os.path.join(os.getcwd(), net_folder, net_filename)
43
44
# read command line options
45
try:
46
opts, args = getopt.getopt(sys.argv[1:], 'f:')
47
except getopt.GetoptError as err:
48
print str(err)
49
sys.exit()
50
for opt,arg in opts:
51
if opt == '-f':
52
net_filename = arg
53
net_filepath = os.path.join(os.getcwd(), net_folder, net_filename)
54
# check if this file exists
55
if not os.path.isfile(net_filepath):
56
print "{} does not exist".format(net_filename)
57
sys.exit()
58
# parse the network size
59
net_size = int(net_filename.split('-')[0])
60
61
# read the network from file
62
nodes_tri = []
63
# nodes_tri: node positions in the triangle grid network
64
# nodes_cart: node positions in Cartesian coordinates
65
# nodes_disp: node positions for display
66
f = open(net_filepath, 'r')
67
new_line = f.readline()
68
while len(new_line) != 0:
69
pos_str = new_line[0:-1].split(' ')
70
pos = [int(pos_str[0]), int(pos_str[1])]
71
nodes_tri.append(pos)
72
new_line = f.readline()
73
74
# generate the connection matrix, 0 for not connected, 1 for connected
75
connections = np.zeros((net_size, net_size))
76
for i in range(net_size):
77
for j in range(i+1, net_size):
78
diff_x = nodes_tri[i][0] - nodes_tri[j][0]
79
diff_y = nodes_tri[i][1] - nodes_tri[j][1]
80
if abs(diff_x) + abs(diff_y) == 1 or diff_x * diff_y == -1:
81
connections[i,j] = 1
82
connections[j,i] = 1
83
# connection list indexed by node
84
connection_lists = []
85
for i in range(net_size):
86
connection_lists.append(list(np.where(connections[i] == 1)[0]))
87
88
# plot the network as dots and lines in pygame window
89
pygame.init()
90
font = pygame.font.SysFont("Cabin", 14)
91
nodes_cart = np.array([trigrid_to_cartesian(pos) for pos in nodes_tri])
92
# find appropriate window size to fit current network
93
(xmin, ymin) = np.amin(nodes_cart, axis=0)
94
(xmax, ymax) = np.amax(nodes_cart, axis=0)
95
clearance = 2.0
96
world_size = (xmax-xmin + clearance, ymax-ymin + clearance)
97
pixels_per_length = 50 # corresponds to 1.0 length in cartesian world
98
screen_size = (int(round(world_size[0] * pixels_per_length)),
99
int(round(world_size[1] * pixels_per_length)))
100
node_size = 8
101
line_width = 4
102
converge_ring_size = 12
103
# colors
104
color_white = (255,255,255)
105
color_black = (0,0,0)
106
distinct_color_set = ((230,25,75), (60,180,75), (255,225,25), (0,130,200), (245,130,48),
107
(145,30,180), (70,240,240), (240,50,230), (210,245,60), (250,190,190),
108
(0,128,128), (230,190,255), (170,110,40), (255,250,200), (128,0,0),
109
(170,255,195), (128,128,0), (255,215,180), (0,0,128), (128,128,128))
110
color_quantity = len(distinct_color_set)
111
# simulation window
112
icon = pygame.image.load("icon_geometry_art.jpg")
113
pygame.display.set_icon(icon)
114
screen = pygame.display.set_mode(screen_size)
115
pygame.display.set_caption("Role Assignment on 2D Triangle Grid Network")
116
117
# node display positions
118
center_temp = (nodes_cart.max(axis=0) + nodes_cart.min(axis=0))/2.0
119
nodes_cart = nodes_cart - center_temp + (world_size[0]/2.0, world_size[1]/2.0)
120
nodes_disp = [world_to_display(nodes_cart[i], world_size, screen_size)
121
for i in range(net_size)]
122
123
# draw the network for the first time
124
screen.fill(color_white)
125
for i in range(net_size):
126
for j in range(i+1, net_size):
127
if connections[i,j]:
128
pygame.draw.line(screen, color_black, nodes_disp[i], nodes_disp[j], line_width)
129
for i in range(net_size):
130
pygame.draw.circle(screen, color_black, nodes_disp[i], node_size, 0)
131
# text = font.render(str(i), True, color_black)
132
# screen.blit(text, (nodes_disp[i][0]+12, nodes_disp[i][1]-12))
133
pygame.display.update()
134
135
raw_input("press <ENTER> to continue")
136
137
########## the role assignment algorithm ##########
138
139
# Gradient value method for information flow control:
140
# This gradient method was first used in the early simulations of Kilobot project, for a robot
141
# to localize itself in a swarm in order to do formation control. The gradient value indicates
142
# the distance from a particular source, and can be used here to guarantee the information
143
# flows only forward, instead of backward. If the source node has gradient value of 0, all the
144
# nodes next to it will have gradient value of 1, then nodes next to them have gradient value
145
# of 2. These nodes are forming nested-ring patterns. The message will only be transmitted from
146
# a low gradient node to a high gradient one. In this way, one message from source will travel
147
# through all other nodes, without resonating infinitely inside the network. Since every node
148
# in this application will transmit its own message, the node needs to calculate the gradient
149
# value of all message sources.
150
151
# To construct the gradient values in a distributed way, when messages are received from a new
152
# message source, the node will take the minimum gradient value plus 1 as its gradient for
153
# that message source. In this way each node will build the gradient values on-the-go for any
154
# other message source. A little more complicated algorithm for constructing gradient values
155
# is also developed to deal with any unstable communication for message transmissions.
156
157
# However, to simplify the role assignment simulation, the gradient map is pre-calculated.
158
# Although I could use algorithm similar in the holistic dependency calculation, a new one that
159
# searching the shortest path between any two nodes is investigated in the following.
160
gradients = np.copy(connections) # build gradient map on the connection map
161
# gradients[i,j] indicates gradient value of node j, to message source i
162
pool_gradient = 1 # gradients of the connections in the pool
163
pool_conn = {}
164
for i in range(net_size):
165
pool_conn[i] = connection_lists[i][:] # start with gradient 1 connections
166
while len(pool_conn.keys()) != 0:
167
source_deactivate = []
168
for source in pool_conn:
169
targets_temp = [] # the new targets
170
for target in pool_conn[source]:
171
for target_new in connection_lists[target]:
172
if target_new == source: continue # skip itself
173
if gradients[source, target_new] == 0:
174
gradients[source, target_new] = pool_gradient + 1
175
targets_temp.append(target_new)
176
if len(targets_temp) == 0:
177
source_deactivate.append(source)
178
else:
179
pool_conn[source] = targets_temp[:] # update with new targets
180
for source in source_deactivate:
181
pool_conn.pop(source) # remove the finished sources
182
pool_gradient = pool_gradient + 1
183
184
# calculate the relative gradient values
185
gradients_rel = []
186
# gradients_rel[i][j,k] refers to gradient of k relative to j with message source i
187
for i in range(net_size): # message source i
188
gradient_temp = np.zeros((net_size, net_size))
189
for j in range(net_size): # in the view point of j
190
gradient_temp[j] = gradients[i] - gradients[i,j]
191
gradients_rel.append(gradient_temp)
192
193
# list the neighbors a node can send message to regarding a message source
194
neighbors_send = [[[] for j in range(net_size)] for i in range(net_size)]
195
# neighbors_send[i][j][k] means, if message from source i is received in j,
196
# it should be send to k
197
for i in range(net_size): # message source i
198
for j in range(net_size): # in the view point of j
199
for neighbor in connection_lists[j]:
200
if gradients_rel[i][j,neighbor] == 1:
201
neighbors_send[i][j].append(neighbor)
202
203
# generate the initial preference distribution
204
pref_dist = np.random.rand(net_size, net_size) # no need to normalize it
205
initial_roles = np.argmax(pref_dist, axis=1) # the chosen role
206
207
# the local assignment information
208
local_role_assignment = [[[-1, 0, -1] for j in range(net_size)] for i in range(net_size)]
209
# local_role_assignment[i][j] is local assignment information of node i for node j
210
# first number is chosen role, second is probability, third is time stamp
211
local_node_assignment = [[[] for j in range(net_size)] for i in range(net_size)]
212
# local_node_assignment[i][j] is local assignment of node i for role j
213
# contains a list of nodes that choose role j
214
# populate the chosen role of itself to the local assignment information
215
for i in range(net_size):
216
local_role_assignment[i][i][0] = initial_roles[i]
217
local_role_assignment[i][i][1] = pref_dist[i, initial_roles[i]]
218
local_role_assignment[i][i][2] = 0
219
local_node_assignment[i][initial_roles[i]].append(i)
220
221
# received message container for all nodes
222
message_rx = [[] for i in range(net_size)]
223
# for each message entry, it containts:
224
# message[0]: ID of message source
225
# message[1]: its preferred role
226
# message[2]: probability of chosen role
227
# message[3]: time stamp
228
# all nodes transmit once their chosen role before the loop
229
transmission_total = 0 # count message transmissions for each iteration
230
iter_count = 0 # also used as time stamp in message
231
for source in range(net_size):
232
chosen_role = local_role_assignment[source][source][0]
233
message_temp = [source, chosen_role, pref_dist[source, chosen_role], iter_count]
234
for target in connection_lists[source]: # send to all neighbors
235
message_rx[target].append(message_temp)
236
transmission_total = transmission_total + 1
237
role_color = [0 for i in range(net_size)] # colors for a conflicting role
238
# Dynamically manage color for conflicting nodes is unnecessarily complicated, might as
239
# well assign the colors in advance.
240
role_index_pool = range(net_size)
241
random.shuffle(role_index_pool)
242
color_index_pool = range(color_quantity)
243
random.shuffle(color_index_pool)
244
while len(role_index_pool) != 0:
245
role_color[role_index_pool[0]] = color_index_pool[0]
246
role_index_pool.pop(0)
247
color_index_pool.pop(0)
248
if len(color_index_pool) == 0:
249
color_index_pool = range(color_quantity)
250
random.shuffle(color_index_pool)
251
252
# flags
253
transmit_flag = [[False for j in range(net_size)] for i in range(net_size)]
254
# whether node i should transmit received message of node j
255
change_flag = [False for i in range(net_size)]
256
# whether node i should change its chosen role
257
scheme_converged = [False for i in range(net_size)]
258
259
sim_exit = False
260
sim_pause = False
261
time_now = pygame.time.get_ticks()
262
time_last = time_now
263
time_period = 2000
264
speed_control = True # set False to skip speed control
265
flash_delay = 200
266
while not sim_exit:
267
# exit the program by close window button, or Esc or Q on keyboard
268
for event in pygame.event.get():
269
if event.type == pygame.QUIT:
270
sim_exit = True # exit with the close window button
271
if event.type == pygame.KEYUP:
272
if event.key == pygame.K_SPACE:
273
sim_pause = not sim_pause # reverse the pause flag
274
if (event.key == pygame.K_ESCAPE) or (event.key == pygame.K_q):
275
sim_exit = True # exit with ESC key or Q key
276
277
# skip the rest if paused
278
if sim_pause: continue
279
280
# simulation speed control
281
time_now = pygame.time.get_ticks()
282
if (time_now - time_last) > time_period:
283
time_last = time_now
284
else:
285
continue
286
287
iter_count = iter_count + 1
288
289
# process the received messages
290
# transfer messages to the processing buffer, then empty the message receiver
291
message_rx_buf = [[[k for k in j] for j in i] for i in message_rx]
292
message_rx = [[] for i in range(net_size)]
293
yield_nodes = [] # the nodes that are yielding on chosen roles
294
yield_roles = [] # the old roles of yield_nodes before yielding
295
for i in range(net_size): # messages received by node i
296
for message in message_rx_buf[i]:
297
source = message[0]
298
role = message[1]
299
probability = message[2]
300
time_stamp = message[3]
301
if source == i:
302
print "error, node {} receives message of itself".format(i)
303
sys.exit()
304
if time_stamp > local_role_assignment[i][source][2]:
305
# received message will only take any effect if time stamp is new
306
# update local_node_assignment
307
role_old = local_role_assignment[i][source][0]
308
if role_old >= 0: # has been initialized before, not -1
309
local_node_assignment[i][role_old].remove(source)
310
local_node_assignment[i][role].append(source)
311
# update local_role_assignment
312
local_role_assignment[i][source][0] = role
313
local_role_assignment[i][source][1] = probability
314
local_role_assignment[i][source][2] = time_stamp
315
transmit_flag[i][source] = True
316
# check conflict with itself
317
if role == local_role_assignment[i][i][0]:
318
if probability >= pref_dist[i, local_role_assignment[i][i][0]]:
319
# change its choice after all message received
320
change_flag[i] = True
321
yield_nodes.append(i)
322
yield_roles.append(local_role_assignment[i][i][0])
323
# change the choice of role for those decide to
324
for i in range(net_size):
325
if change_flag[i]:
326
change_flag[i] = False
327
role_old = local_role_assignment[i][i][0]
328
pref_dist_temp = np.copy(pref_dist[i])
329
pref_dist_temp[local_role_assignment[i][i][0]] = -1
330
# set to negative to avoid being chosen
331
for j in range(net_size):
332
if len(local_node_assignment[i][j]) != 0:
333
# eliminate those choices that have been taken
334
pref_dist_temp[j] = -1
335
role_new = np.argmax(pref_dist_temp)
336
if pref_dist_temp[role_new] < 0:
337
print "error, node {} has no available role".format(i)
338
sys.exit()
339
# role_new is good to go
340
# update local_node_assignment
341
local_node_assignment[i][role_old].remove(i)
342
local_node_assignment[i][role_new].append(i)
343
# update local_role_assignment
344
local_role_assignment[i][i][0] = role_new
345
local_role_assignment[i][i][1] = pref_dist[i][role_new]
346
local_role_assignment[i][i][2] = iter_count
347
transmit_flag[i][i] = True
348
# transmit the received messages or initial new message transmission
349
transmission_total = 0
350
for transmitter in range(net_size): # transmitter node
351
for source in range(net_size): # message is for this source node
352
if transmit_flag[transmitter][source]:
353
transmit_flag[transmitter][source] = False
354
message_temp = [source, local_role_assignment[transmitter][source][0],
355
local_role_assignment[transmitter][source][1],
356
local_role_assignment[transmitter][source][2]]
357
for target in neighbors_send[source][transmitter]:
358
message_rx[target].append(message_temp)
359
transmission_total = transmission_total + 1
360
361
# check if role assignment scheme is converged at individual node
362
for i in range(net_size):
363
if not scheme_converged[i]:
364
converged = True
365
for j in range(net_size):
366
if len(local_node_assignment[i][j]) != 1:
367
converged = False
368
break
369
if converged:
370
scheme_converged[i] = True
371
372
# for display, scan the nodes that have detected conflict but not yielding
373
persist_nodes = []
374
for i in range(net_size):
375
if i in yield_nodes: continue
376
if len(local_node_assignment[i][local_role_assignment[i][i][0]]) > 1:
377
persist_nodes.append(i)
378
379
# debug print
380
print "iteration {}, total transmission {}".format(iter_count, transmission_total)
381
382
# update the display
383
for i in range(net_size):
384
for j in range(i+1, net_size):
385
if connections[i,j]:
386
pygame.draw.line(screen, color_black, nodes_disp[i], nodes_disp[j], line_width)
387
for i in range(net_size):
388
pygame.draw.circle(screen, color_black, nodes_disp[i], node_size, 0)
389
# draw the persisting nodes with color of conflicting role
390
for i in persist_nodes:
391
pygame.draw.circle(screen, distinct_color_set[role_color[local_role_assignment[i][i][0]]],
392
nodes_disp[i], node_size, 0)
393
# draw extra ring on node if local scheme has converged
394
for i in range(net_size):
395
if scheme_converged[i]:
396
pygame.draw.circle(screen, color_black, nodes_disp[i], converge_ring_size, 2)
397
pygame.display.update()
398
# flash the yielding nodes with color of old role
399
for _ in range(3):
400
# change to color
401
for i in range(len(yield_nodes)):
402
pygame.draw.circle(screen, distinct_color_set[role_color[yield_roles[i]]],
403
nodes_disp[yield_nodes[i]], node_size, 0)
404
pygame.display.update()
405
pygame.time.delay(flash_delay)
406
# change to black
407
for i in range(len(yield_nodes)):
408
pygame.draw.circle(screen, color_black,
409
nodes_disp[yield_nodes[i]], node_size, 0)
410
pygame.display.update()
411
pygame.time.delay(flash_delay)
412
413
# exit the simulation if all role assignment schemes have converged
414
all_converged = scheme_converged[0]
415
for i in range(1, net_size):
416
all_converged = all_converged and scheme_converged[i]
417
if not all_converged: break
418
if all_converged: sim_exit = True
419
420
# hold the simulation window to exit manually
421
raw_input("role assignment finished, press <ENTER> to exit")
422
423
424
425
426