Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
yangliu28
GitHub Repository: yangliu28/swarm_formation_sim
Path: blob/master/loop_formation.py
104 views
1
# loop formation simulation of swarm robots, form a triangle, and build loop by merging
2
3
# comments for programming purpose:
4
# Similar status design with the second line formation:
5
# '0': free, wondering around, available to form a pair, a triangle or joint a group
6
# '1': transit status between '0' and '2', robot in a group, but not a loop yet
7
# '1_0': forming status, either in a pair, or in a triangle formation
8
# '1_0_0': forming status, in a pair
9
# '1_0_1': forming status, in a triangle
10
# '1_1': merging status, joining a robot '2' group
11
# '2': formal members of a loop formation group
12
# '-1': free, wondering around, ignoring all interactions
13
# Only allowed status transitions are:
14
# forward: '-1'->'0', '0'->'1_0_0', '0'->'1_0_1', '0'->'1_1', '1_0_0'->'1_0_1'
15
# '1_0_1'->'2', '1_1'->2
16
# backward: '1'->'-1', '2'->'-1'
17
# Status transition regarding robot '1':
18
# When 2 '0' meets, they both become '1_0_0', and start adjust distance to the loop
19
# space. When distance is good, they don't perform any status transition yet. Only
20
# when a new '0' comes over, it will trigger all three robots into '1_0_1' status.
21
# And during the entire time of '1_0_0', the two robots are always welcoming new '0',
22
# even their distance is not good yet. When in '1_0_1', after the three robots form
23
# the perfect triangle and good loop space, they will trigger a status transition
24
# of becoming '2'.
25
# Two-stage forming explanation:
26
# It's likely to happen that a '0' meets two '0' at the same time, especially at the
27
# the begining of the simulation. It would be conveniently to form a triangle directly
28
# if this happens. But I stick with two stage forming, to have a pair before the triangle.
29
# Mainly because it makes programming simplier, and it takes another loop to turn the
30
# pair to a triangle. And also in real robots, the pair will be established before the
31
# triangle.
32
33
# There is no index indicating where the robot is on the loop, that means each robot's
34
# role is equal. Each has two neighbors, one on left, one on right. There is no start or
35
# end on the loop.
36
37
# A simple loop adjusting method was used. Each robot keeps moving to achieve the desired
38
# loop space. In the meantime, if a robot on the loop meets another robot on the same loop
39
# that is not one of its key neighbors, it moves away from that robot, using the old velocity
40
# for the loop space destination.
41
42
43
import pygame
44
import math, random, sys
45
from loop_formation_robot import LFRobot
46
from formation_functions import *
47
48
pygame.init()
49
50
# for display, window origin is at left up corner
51
screen_size = (1200, 1000) # width and height
52
background_color = (0, 0, 0) # black background
53
robot_0_color = (0, 255, 0) # for robot status '0', green
54
robot_1_0_0_color = (255, 153, 153) # for robot status '1_0_0', light pink
55
robot_1_0_1_color = (255, 102, 102) # for robot status '1_0_1', dark pink
56
robot_1_1_color = (255, 102, 102) # for robot status '1_1', dark pink
57
robot_2_color = (255, 0, 0) # for robot status '2', red
58
robot_n1_color = (0, 0, 102) # for robot status '-1', blue
59
robot_size = 5 # robot modeled as dot, number of pixels in radius
60
61
# set up the simulation window and surface object
62
icon = pygame.image.load('icon_geometry_art.jpg')
63
pygame.display.set_icon(icon)
64
screen = pygame.display.set_mode(screen_size)
65
pygame.display.set_caption('Loop Formation Simulation')
66
67
# for physics, continuous world, origin is at left bottom corner, starting (0, 0),
68
# with x axis pointing right, y axis pointing up.
69
# It's more natural to compute the physics in right hand coordiante system.
70
world_size = (100.0, 100.0 * screen_size[1]/screen_size[0])
71
72
# variables to configure the simulation
73
robot_quantity = 30
74
distrib_coef = 0.5 # coefficient to resize the initial robot distribution
75
const_vel = 4.0 # all robots except those in adjusting phase are moving at this speed
76
frame_period = 100 # updating period of the simulation and graphics, in millisecond
77
comm_range = 5.0 # sensing and communication share this same range, in radius
78
loop_space = comm_range * 0.75 # desired space of robots on the lop
79
space_error = loop_space * 0.2 # error to determine if a status transition is needed
80
life_incre = 10 # number of seconds a new member adds to a group
81
group_id_upper_limit = 1000 # upper limit of random integer for group id
82
n1_life_lower = 3 # lower limit of life time for status '-1'
83
n1_life_upper = 8 # upper limit of life time for status '-1'
84
# coefficient for calculating velocity of robot '2' on the loop for adjusting
85
# as the distance error decreases, the loop adjusting velocity also decreases
86
adjust_vel_coef = const_vel/loop_space * 0.8
87
88
# instantiate the robot swarm as list
89
robots = [] # container for all robots, index is its identification
90
for i in range(robot_quantity):
91
# random position, away from the window's edges
92
pos_temp = (((random.random()-0.5)*distrib_coef+0.5) * world_size[0],
93
((random.random()-0.5)*distrib_coef+0.5) * world_size[1])
94
vel_temp = const_vel
95
ori_temp = random.random() * 2*math.pi - math.pi # random in (-pi, pi)
96
object_temp = LFRobot(pos_temp, vel_temp, ori_temp)
97
robots.append(object_temp)
98
# instantiate the group variable as dictionary
99
groups = {}
100
# key is the group id
101
# value is a list
102
# 0.first element: the group size, all members including both '2' and '1'
103
# 1.second element: the number of robots on the loop, only robot '2'
104
# 2.third element: a list of robots on the loop, nor ordered, status '2'
105
# 3.fourth element: a list of robots off the loop, not ordered, status '1'
106
# 4.second element: remaining life time
107
# 5.fifth element: true or false of being the dominant group
108
# instantiate a distance table for every pair of robots
109
# make sure all data in table is being written when updating
110
dist_table = [[0 for j in range(robot_quantity)] for i in range(robot_quantity)]
111
112
# function for solving destination on the loop based on positions of two neighbors
113
def des_solver(pos_l, pos_r, dist_0, l_d):
114
# first input is 2D position of neighbor on the left, second for on the right
115
# dist_0 is the distance between pos_l and pos_r
116
# l_d is the desired distance to the two neighbors
117
vect_0 = (pos_l[0]-pos_r[0], pos_l[1]-pos_r[1]) # vector from pos_r to pos_l
118
midpoint = [(pos_l[0]+pos_r[0])/2, (pos_l[1]+pos_r[1])/2]
119
if dist_0 >= 2*l_d or dist_0 < 0:
120
# two neighbors are too far away, destination will be just midpoint
121
# dist_0 will be passed from dist_table, just in case if -1 is accidentally passed
122
return midpoint
123
else:
124
# get direction perpendicular to the the line of pos_l and pos_r
125
vect_1 = [-vect_0[1]/dist_0, vect_0[0]/dist_0] # rotate vect_0 ccw for 90 degrees
126
dist_1 = math.sqrt(l_d*l_d - dist_0*dist_0/4)
127
# return the point from midpoint, goes along vect_1 for dist_1
128
return [midpoint[0]+vect_1[0]*dist_1, midpoint[1]+vect_1[1]*dist_1]
129
130
# the loop
131
sim_exit = False # simulation exit flag
132
sim_pause = True # simulation pause flag
133
timer_last = pygame.time.get_ticks() # return number of milliseconds after pygame.init()
134
timer_now = timer_last # initialize it with timer_last
135
while not sim_exit:
136
# exit the program
137
for event in pygame.event.get():
138
if event.type == pygame.QUIT:
139
sim_exit = True # exit with the close window button
140
if event.type == pygame.KEYUP:
141
if event.key == pygame.K_SPACE:
142
sim_pause = not sim_pause # reverse the pause flag
143
if (event.key == pygame.K_ESCAPE) or (event.key == pygame.K_q):
144
sim_exit = True # exit with ESC key or Q key
145
146
# skip the rest of the loop if paused
147
if sim_pause: continue
148
149
# update the physics, control rules and graphics at the same time
150
timer_now = pygame.time.get_ticks()
151
if (timer_now - timer_last) > frame_period:
152
timer_last = timer_now # reset timer
153
# prepare the distance data for every pair of robots
154
for i in range(robot_quantity):
155
for j in range(i+1, robot_quantity):
156
# status of '-1' does not involve in any connection, so skip
157
if (robots[i].status == -1) or (robots[j].status == -1):
158
dist_table[i][j] = -1.0
159
dist_table[j][i] = -1.0
160
continue # skip the rest
161
# it only covers the upper triangle without the diagonal
162
vect_temp = (robots[i].pos[0]-robots[j].pos[0],
163
robots[i].pos[1]-robots[j].pos[1])
164
dist_temp = math.sqrt(vect_temp[0]*vect_temp[0] +
165
vect_temp[1]*vect_temp[1])
166
if dist_temp <= comm_range:
167
# only record distance smaller than communication range
168
dist_table[i][j] = dist_temp
169
dist_table[j][i] = dist_temp
170
else: # ignore the neighbors too far away
171
dist_table[i][j] = -1.0
172
dist_table[j][i] = -1.0
173
# sort the distance in another table, record the index here
174
index_list = [[] for i in range(robot_quantity)] # index of neighbors in range
175
# find all robots with non-zero distance
176
for i in range(robot_quantity):
177
for j in range(robot_quantity):
178
if j == i: continue # skip the self, not necessary
179
if dist_table[i][j] > 0: index_list[i].append(j)
180
# sort the index_list in the order of increasing distance
181
for i in range(robot_quantity):
182
len_temp = len(index_list[i]) # number of neighbors
183
if (len_temp < 2): continue # no need to sort
184
else:
185
# bubble sort
186
for j in range(len_temp-1):
187
for k in range(len_temp-1-j):
188
if (dist_table[i][index_list[i][k]] >
189
dist_table[i][index_list[i][k+1]]):
190
# swap the position of the two index
191
index_temp = index_list[i][k]
192
index_list[i][k] = index_list[i][k+1]
193
index_list[i][k+1] = index_temp
194
# get the status list corresponds to the sorted index_list
195
status_list = [[] for i in range(robot_quantity)]
196
for i in range(robot_quantity):
197
for j in index_list[i]:
198
status_list[i].append(robots[j].status)
199
200
# instantiate the status transition variables, for the status check process
201
# the priority is in the following order
202
s_merging = {} # robot '0' detects robot '2', merging into its group
203
# key is id of robot '0', value is id of corresponding robot '2'
204
s_forming2 = {} # two '1_0_0' form the triangle with a '0', becoming '1_0_1'
205
# key is id of the robot '0', value is the group id of the pair '1_0_0'
206
# it is designed to be triggered by the new robot '0'
207
s_forming1 = {} # robot '0' forms the pair with another '0', becoming '1_0_0'
208
# key is id of robot '0' that discovers other robots '0' in range
209
# value is a list of robots '0' that are detected, in order of ascending dist
210
# this variable works the same with "s_init_form" from previous simulations
211
s_form_done = [] # robot '1_0_1' finishes initial forming, becoming '2'
212
# list of group id that the triangle forming is done
213
s_form_checked = [] # auxiliary variable for "s_form_done", indicating checked groups
214
s_merge_done = [] # robot '1_1' finishes merging, becoming '2'
215
# list of robots '1' that finished merging
216
s_group_exp = [] # life time of a group naturally expires, disassemble it
217
# list of group ids
218
s_disassemble = [] # disassemble trigger by robots '1' or '2'
219
# list of lists of group id to be compared for disassembling
220
s_back_0 = [] # robot '-1' gets back to '0' after life expires
221
# list of robots '-1'
222
# All the checkings for robots '1' and '2' getting lost during forming are removed,
223
# such behaviors should be observable during tests, and program should not run into
224
# any robot lost once it is free of bugs. Therefore it's not necessary for checking.
225
226
# check 'robots' variable for any status change, schedule for processing in next step
227
for i in range(robot_quantity):
228
# for the host robot having status of '0'
229
if robots[i].status == 0:
230
# check if this robot has valid neighbors at all
231
if len(index_list[i]) == 0: continue # skip the neighbor check
232
# pre-process: classify the robot '1' neighbors into their substatus,
233
# because robot '0' reacts differently to different substatuses of '1'
234
# new statuses have been assigned as follows:
235
# '1_0_0' -> '1.1'
236
# '1_0_1' -> '1.2'
237
# '1_1' -> '1.3'
238
if 1 in status_list[i]:
239
index_temp = 0
240
while 1 in status_list[i]:
241
index_temp = status_list[i].index(1)
242
robot_temp = index_list[i][index_temp]
243
status_1_sub = robots[robot_temp].status_1_sub
244
if status_1_sub == 0:
245
status_1_0_sub = robots[robot_temp].status_1_0_sub
246
if status_1_0_sub == 0:
247
status_list[i][index_temp] = 1.1 # 1.1 for status '1_0_0'
248
elif status_1_0_sub == 1:
249
status_list[i][index_temp] = 1.2 # 1.2 for status '1_0_1'
250
else: # just in case
251
print("wrong sub status of 1_0")
252
sys.exit()
253
elif status_1_sub == 1:
254
status_list[i][index_temp] = 1.3 # 1.3 for status '1_1'
255
else: # just in case
256
prin("wrong sub status of 1")
257
sys.exit()
258
# the sequence of processing the neighbors are:
259
# '2' -> '1.1' -> '0' -> '1.2'&'1.3'
260
# This means if there is a group of '2', join them; otherwise if there is a
261
# pair of '1_0_0', join them; otherwise if there is a '0', pair with it; if
262
# all the above failed, just get bounced away by closest '1_0_1' or '1_1'
263
# start processing neighbors with status '2'
264
if 2 in status_list[i]:
265
# check the group attribution of all the '2'
266
# get the robot id and group id of the first '2'
267
index_temp = status_list[i].index(2)
268
robot_temp = index_list[i][index_temp]
269
group_temp = robots[robot_temp].group_id
270
groups_temp = {group_temp: [robot_temp]}
271
# check if there are still '2' in the list
272
while 2 in status_list[i][index_temp+1:]:
273
# indexing the next '2' from index_temp+1
274
# update the index_temp, robot_temp, group_temp
275
index_temp = status_list[i].index(2, index_temp+1)
276
robot_temp = index_list[i][index_temp]
277
group_temp = robots[robot_temp].group_id
278
# update groups_temp
279
if group_temp in groups_temp.keys():
280
# append this robot in the existing group
281
groups_temp[group_temp].append(robot_temp)
282
else:
283
# add new group id in groups_temp and add this robot
284
groups_temp[group_temp] = [robot_temp]
285
# check if there are multiple groups detected from the '2'
286
target_robot = 0 # the target robot '2' to merge into the group
287
if len(groups_temp.keys()) == 1:
288
# there is only one group detected
289
dist_min = 2*comm_range # smallest distance, start with large one
290
robot_min = -1 # corresponding robot with min distance
291
# search the closest '2'
292
for j in groups_temp.values()[0]:
293
if dist_table[i][j] < dist_min and dist_table[i][j] > 0:
294
if robots[j].status_2_avail1:
295
# at least the interior angle of robot j should be good
296
it0 = robots[j].key_neighbors[0] # the two neighbors
297
it1 = robots[j].key_neighbors[1]
298
if ((robots[it0].status_2_avail1 and robots[j].status_2_avail2[0]) or
299
(robots[it1].status_2_avail1 and robots[j].status_2_avail2[1])):
300
# check both avail1 of neighbors and avail2 of robot j
301
# there should be at least one side available to be merged
302
dist_min = dist_table[i][j]
303
robot_min = j
304
target_robot = robot_min
305
else:
306
# there is more than one group detected
307
# it is designed that no disassembling trigger from robot '0'
308
# compare which group has the most members in it
309
member_max = 0 # start with 0 number of members in group
310
group_max = 0 # corresponding group id with most members
311
for g_it in groups_temp.keys():
312
if groups[g_it][0] > member_max:
313
member_max = groups[g_it][0]
314
group_max = g_it
315
# search the closest '2' inside that group
316
dist_min = 2*comm_range
317
robot_min = -1
318
for j in groups_temp[group_max]:
319
if dist_table[i][j] < dist_min and dist_table[i][j] > 0:
320
if robots[j].status_2_avail1:
321
# at least the interior angle of robot j should be good
322
it0 = robots[j].key_neighbors[0] # the two neighbors
323
it1 = robots[j].key_neighbors[1]
324
if ((robots[it0].status_2_avail1 and robots[j].status_2_avail2[0]) or
325
(robots[it1].status_2_avail1 and robots[j].status_2_avail2[1])):
326
dist_min = dist_table[i][j]
327
robot_min = j
328
target_robot = robot_min
329
# check if target robot has been located, prepare to merge into its group
330
if target_robot != -1: # not the initial value of robot_min
331
# the target robot should have at least one spot availbe to be merged
332
# status transition scheduled, robot '0' is becoming '1_1'
333
s_merging[i] = target_robot
334
# process neighbors with status '1.1'
335
elif 1.1 in status_list[i]:
336
# check the group attribution of all '1.1'
337
# get the robot id and group id of the first 1.1
338
index_temp = status_list[i].index(1.1)
339
robot_temp = index_list[i][index_temp]
340
group_temp = robots[robot_temp].group_id
341
groups_temp = {group_temp: [robot_temp]}
342
# check if there are still '1.1' in the list
343
while 1.1 in status_list[i][index_temp+1:]:
344
# update the index_temp, robot_temp, group_temp
345
index_temp = status_list[i].index(1.1, index_temp+1)
346
robot_temp = index_list[i][index_temp]
347
group_temp = robots[robot_temp].group_id
348
# update groups_temp
349
if group_temp in groups_temp.keys():
350
# append this robot in the existing group
351
groups_temp[group_temp].append(robot_temp)
352
else:
353
# add new entry of group id in groups_temp and add this robot
354
groups_temp[group_temp] = [robot_temp]
355
# check if there are multiple groups detected from the '1.1'
356
target_robot = 0 # the target robot '1.1' to form triangle with
357
if len(groups_temp.keys()) == 1:
358
# there is only one group detected from the '1.1'
359
dist_min = 2*comm_range # variable for smallest distance
360
robot_min = -1 # corresponding robot with dist_min
361
# search the closest '1.1'
362
for j in groups_temp.values()[0]:
363
if dist_table[i][j] < dist_min and dist_table[i][j] > 0:
364
# there is no availability restriction here
365
dist_min = dist_table[i][j]
366
robot_min = j
367
target_robot = robot_min
368
else:
369
# there are more than one group detected from the '1.1'
370
# choose the group that has the most members in it
371
member_max = 0 # variable for the maximum number of members
372
group_max = 0 # corresponding group id with member_max
373
for g_it in groups_temp.keys():
374
if groups[g_it][0] > member_max:
375
member_max = groups[g_it][0]
376
group_max = g_it
377
# search the closest '1.1' inside that group "group_max"
378
dist_min = 2*comm_range
379
robot_min = -1
380
for j in groups_temp[group_max]:
381
if dist_table[i][j] < dist_min and dist_table[i][j] > 0:
382
dist_min = dist_table[i][j]
383
robot_min = j
384
target_robot = robot_min
385
# check if target robot has been located, form the triangle with it
386
if target_robot != -1: # not the initial value of "robot_min"
387
group_temp = robots[target_robot].group_id
388
# status transition scheduled, robot '0' & '1_0_0' are becoming '1_0_1'
389
s_forming2[i] = group_temp
390
# it's possible that more than 1 of robot '0' are working with same group
391
# the first robot '0' that gets processed will win
392
# process neighbors with status '0'
393
elif 0 in status_list[i]:
394
# establish a list of all '0', in order of ascending distance
395
# this list is to be checked later if pairing is possible and no conflict
396
# first remove possible '1.2' and '1.3' from status_list[i]
397
while 1.2 in status_list[i]:
398
index_temp = status_list[i].index(1.2)
399
status_list[i].pop(index_temp)
400
index_list[i].pop(index_temp)
401
while 1.3 in status_list[i]:
402
index_temp = status_list[i].index(1.3)
403
status_list[i].pop(index_temp)
404
index_list[i].pop(index_temp)
405
# status transition scheduled, robot '0' is becoming '1_0_0'
406
s_forming1[i] = index_list[i][:]
407
# process neighbors with status '1.2' and '1.3'
408
elif (1.2 in status_list[i]) or (1.3 in status_list[i]):
409
# find the closest '1.2' or '1.3', and get bounced away by it
410
dist_min = 2*comm_range
411
robot_min = -1
412
# at this time, '1.2' and '1.3' are the only robots left in index_list[i]
413
for j in index_list[i]:
414
if dist_table[i][j] < dist_min and dist_table[i][j] > 0:
415
dist_min = dist_table[i][j]
416
robot_min = j
417
# target robot located, the robot_min, should not still be '-1' here
418
# get bounced away from this robot, update the moving direction
419
vect_temp = (robots[i].pos[0] - robots[robot_min].pos[0],
420
robots[i].pos[1] - robots[robot_min].pos[1])
421
# new orientation is pointing from robot_min to the host robot
422
robots[i].ori = math.atan2(vect_temp[1], vect_temp[0])
423
# for the host robot having status of '1'
424
elif robots[i].status == 1:
425
# disassemble check, get group attribution of all '1' and '2'
426
# pop out the '0' from the status_list
427
while 0 in status_list[i]:
428
index_temp = status_list[i].index(0)
429
status_list[i].pop(index_temp)
430
index_list[i].pop(index_temp)
431
if len(index_list[i]) > 0: # ensure there is at least one robot around
432
# start the group attribution dictionaary with first robot
433
robot_temp = index_list[i][0]
434
group_temp = robots[robot_temp].group_id
435
groups_temp = {group_temp:[robot_temp]}
436
# then check the rest for group attribution
437
for j in index_list[i][1:]:
438
group_temp = robots[j].group_id
439
if group_temp in groups_temp.keys():
440
groups_temp[group_temp].append(j)
441
else:
442
groups_temp[group_temp] = [j]
443
# check if there are multiple groups detected
444
if len(groups_temp.keys()) > 1:
445
# status transition scheduled, to disassemble the minority groups
446
s_disassemble.append(groups_temp.keys())
447
# may produce duplicates in s_disassemble, not big problem
448
# check if any status transition needs to be scheduled
449
if robots[i].status_1_sub == 0:
450
if robots[i].status_1_0_sub == 1:
451
# host robot is in the triangle forming phase
452
# check if this group has already been scheduled for status transition
453
g_it = robots[i].group_id
454
if g_it not in s_form_checked:
455
# avoid checking same group three times
456
s_form_checked.append(g_it) # add this group, mark as checked
457
it0 = robots[i].key_neighbors[0]
458
it1 = robots[i].key_neighbors[1]
459
# check all threee sides to see if distances are satisfied
460
dist_satisfied = True
461
# if abs(dist_table[i][it0] - loop_space) > space_error:
462
# dist_satisfied = False
463
# elif abs(dist_table[i][it1] - loop_space) > space_error:
464
# dist_satisfied = False
465
# elif abs(dist_table[it0][it1] - loop_space) > space_error:
466
# dist_satisfied = False
467
# release the requirements that as long as three robots can see
468
# each other, they can form the initial loop of triangle
469
if dist_table[i][it0] > comm_range: dist_satisfied = False
470
if dist_table[i][it1] > comm_range: dist_satisfied = False
471
if dist_table[it0][it1] > comm_range: dist_satisfied = False
472
if dist_satisfied:
473
# status transition scheduled, robots '1_0_1' are becoming '2'
474
s_form_done.append(g_it)
475
elif robots[i].status_1_sub == 1:
476
# robot is in the merging phase
477
it0 = robots[i].key_neighbors[0]
478
it1 = robots[i].key_neighbors[1]
479
# check the two distances to see if they are satisfied
480
dist_satisfied = True
481
if abs(dist_table[i][it0] - loop_space) > space_error:
482
dist_satisfied = False
483
elif abs(dist_table[i][it1] - loop_space) > space_error:
484
dist_satisfied = False
485
if dist_satisfied:
486
# status transition scheduled, robot '1_1' is becoming '2'
487
s_merge_done.append(i)
488
# for the host robot having status of '2'
489
elif robots[i].status == 2:
490
# disassemble check, get group attribution of all '1' and '2'
491
# pop out the '0' from the status_list
492
while 0 in status_list[i]:
493
index_temp = status_list[i].index(0)
494
status_list[i].pop(index_temp)
495
index_list[i].pop(index_temp)
496
if len(index_list[i]) > 0: # ensure at least one in-group robot around
497
# start the group attribution dictionaary with first robot
498
robot_temp = index_list[i][0]
499
group_temp = robots[robot_temp].group_id
500
groups_temp = {group_temp:[robot_temp]}
501
# then check the rest for group attribution
502
for j in index_list[i][1:]:
503
group_temp = robots[j].group_id
504
if group_temp in groups_temp.keys():
505
groups_temp[group_temp].append(j)
506
else:
507
groups_temp[group_temp] = [j]
508
# check if there are multiple groups detected
509
if len(groups_temp.keys()) > 1:
510
# status transition scheduled, to disassemble the minority groups
511
s_disassemble.append(groups_temp.keys())
512
# may produce duplicates in s_disassemble, not big problem
513
# further process the 'status_list[i]' and 'index_list[i]'
514
# for use in loop adjusting when in the physics update
515
# the following removes all '1', and keep same group '2' except key neighbors
516
g_it = robots[i].group_id # group id of host robot
517
while 1 in status_list[i]:
518
index_temp = status_list[i].index(1)
519
status_list[i].pop(index_temp)
520
index_list[i].pop(index_temp)
521
index_temp = 0
522
while 2 in status_list[i][index_temp:]:
523
index_temp = status_list[i].index(2,index_temp)
524
robot_temp = index_list[i][index_temp]
525
if robot_temp in robots[i].key_neighbors:
526
# remove the same group key neighbors
527
status_list[i].pop(index_temp)
528
index_list[i].pop(index_temp)
529
elif robots[robot_temp].group_id != g_it:
530
# remove non-same-group robot '2'
531
status_list[i].pop(index_temp)
532
index_list[i].pop(index_temp)
533
else:
534
# keep the same group '2' that aren't key neighbors
535
index_temp = index_temp + 1
536
# for the host robot having status of '-1'
537
elif robots[i].status == -1:
538
# check if life time expires
539
if robots[i].status_n1_life < 0:
540
# status transition scheduled, robot '-1' is becoming '0'
541
s_back_0.append(i)
542
543
# check 'groups' variable for any status change
544
for g_it in groups.keys():
545
if groups[g_it][5]: continue # already being dominant group
546
if groups[g_it][0] > robot_quantity/2:
547
# the group has more than half the totoal number of robots
548
groups[g_it][5] = True # becoming dominant group
549
groups[g_it][4] = 100.0 # a large number
550
print("dominant group established")
551
if groups[g_it][4] < 0: # life time of a group expires
552
# schedule operation to disassemble this group
553
s_group_exp.append(g_it)
554
555
# process the scheduled status change, in the order of the designed priority
556
# 1.s_merging, robot '0' starts to merge into the group with the robot '2'
557
for i in s_merging.keys():
558
j = s_merging[i] # 'j' is the robot that robot 'i' tries to merge with
559
# the first merge availability of robot 'j' should be true to be here
560
# discuss the merging availability of robot 'j' on the two sides
561
g_it = robots[j].group_id
562
it0 = robots[j].key_neighbors[0]
563
it1 = robots[j].key_neighbors[1]
564
# j was available when added to s_merging, but check again in case occupied
565
# merge availablility on left side
566
side0_avail = robots[it0].status_2_avail1 and robots[j].status_2_avail2[0]
567
side0_des = [-1,-1] # destination if merging at left side
568
if side0_avail:
569
# calculate the merging destination
570
side0_des = des_solver(robots[it0].pos, robots[j].pos,
571
dist_table[it0][j], loop_space)
572
# merge availablility on right side
573
side1_avail = robots[it1].status_2_avail1 and robots[j].status_2_avail2[1]
574
side1_des = [-1,-1]
575
if side1_avail:
576
side1_des = des_solver(robots[j].pos, robots[it1].pos,
577
dist_table[j][it1], loop_space)
578
# check if there is at least one side available
579
if side0_avail or side1_avail:
580
# status transition operations regardless of which side to merge
581
robots[i].status = 1
582
robots[i].status_1_sub = 1
583
robots[i].group_id = g_it
584
robots[i].key_neighbors = [-1,-1] # initialize it with '-1'
585
groups[g_it][0] = groups[g_it][0] + 1 # increase group size by 1
586
groups[g_it][3].append(i)
587
groups[g_it][4] = groups[g_it][4] + life_incre # increase life time
588
# deciding which side to merge
589
side_decision = -1 # 0 for left, 1 for right
590
if side0_avail and side1_avail:
591
# both sides are available, calculate distances and compare
592
vect_temp = (side0_des[0]-robots[i].pos[0], side0_des[1]-robots[i].pos[1])
593
side0_dist = math.sqrt(vect_temp[0]*vect_temp[0]+
594
vect_temp[1]*vect_temp[1])
595
vect_temp = (side1_des[0]-robots[i].pos[0], side1_des[1]-robots[i].pos[1])
596
side1_dist = math.sqrt(vect_temp[0]*vect_temp[0]+
597
vect_temp[1]*vect_temp[1])
598
if side0_dist < side1_dist:
599
side_decision = 0
600
else:
601
side_decision = 1
602
elif side0_avail and (not side1_avail):
603
# only left side is available
604
side_decision = 0
605
elif side1_avail and (not side0_avail):
606
# only right side is available
607
side_decision = 1
608
# status transition operations according to the side decision
609
if side_decision == 0: # taking left side
610
it1 = robots[j].key_neighbors[0]
611
robots[i].key_neighbors = [it1, j]
612
robots[i].status_1_1_des = side0_des
613
vect_temp = (side0_des[0]-robots[i].pos[0], side0_des[1]-robots[i].pos[1])
614
robots[i].ori = math.atan2(vect_temp[1], vect_temp[0]) # update moving ori
615
# update for j and it1
616
robots[j].status_2_avail2[0] = False
617
robots[it1].status_2_avail2[1] = False
618
else: # taking right side
619
it1 = robots[j].key_neighbors[1]
620
robots[i].key_neighbors = [j, it1]
621
robots[i].status_1_1_des = side1_des
622
vect_temp = (side1_des[0]-robots[i].pos[0], side1_des[1]-robots[i].pos[1])
623
robots[i].ori = math.atan2(vect_temp[1], vect_temp[0]) # update moving ori
624
# update for j and it1
625
robots[j].status_2_avail2[1] = False
626
robots[it1].status_2_avail2[0] = False
627
# 2.s_forming2, two '1_0_0' forms a triangle with one '0', all becomes '1_0_1'
628
for i in s_forming2.keys():
629
g_it = s_forming2[i] # get the group id of the group to form the triangle
630
if groups[g_it][0] > 2: continue # the group already accepted a robot '0'
631
# the two group members
632
it0 = groups[g_it][3][0]
633
it1 = groups[g_it][3][1]
634
# deciding robot i is at which side along the vector from it0 to it1
635
vect_0 = (robots[it1].pos[0]-robots[it0].pos[0],
636
robots[it1].pos[1]-robots[it0].pos[1]) # vector from it0 to it1
637
vect_1 = (robots[i].pos[0]-robots[it0].pos[0],
638
robots[i].pos[1]-robots[it0].pos[1]) # vector from it0 to i
639
# calculate cross product of vect_0 and vect_1
640
if (vect_0[0]*vect_1[1]-vect_0[1]*vect_1[0]) > 0:
641
# i is at left side of vector from it0 to it1
642
# this is the desired sequence, it goes ccw from it0 to it1 to i
643
pass
644
else:
645
# i is at right side of vector from it0 to it1
646
# (it's very unlikely that i is on the line of it0 and it1)
647
# swap value of it0 and it1
648
# because variable it1 will be treated as robot on i's left, and it0 right
649
it0 = groups[g_it][3][1]
650
it1 = groups[g_it][3][0]
651
# update the robots[i], the new member
652
robots[i].status = 1
653
robots[i].status_1_sub = 0
654
robots[i].status_1_0_sub = 1
655
robots[i].group_id = g_it
656
# it1 is on left, it0 is on right, because i is the end index robot on loop
657
des_new = des_solver(robots[it1].pos, robots[it0].pos,
658
dist_table[it1][it0], loop_space)
659
robots[i].status_1_0_1_des = des_new
660
vect_temp = (des_new[0]-robots[i].pos[0], des_new[1]-robots[i].pos[1])
661
robots[i].ori = math.atan2(vect_temp[1], vect_temp[0])
662
robots[i].key_neighbors = [it1, it0]
663
# update the robots[it0]
664
robots[it0].status_1_0_sub = 1
665
des_new = des_solver(robots[i].pos, robots[it1].pos,
666
dist_table[i][it1], loop_space)
667
robots[it0].status_1_0_1_des = des_new
668
vect_temp = (des_new[0]-robots[it0].pos[0], des_new[1]-robots[it0].pos[1])
669
robots[it0].ori = math.atan2(vect_temp[1], vect_temp[0])
670
robots[it0].key_neighbors = [i, it1]
671
# update the robots[it1]
672
robots[it1].status_1_0_sub = 1
673
des_new = des_solver(robots[it0].pos, robots[i].pos,
674
dist_table[it0][i], loop_space)
675
robots[it1].status_1_0_1_des = des_new
676
vect_temp = (des_new[0]-robots[it1].pos[0], des_new[1]-robots[it1].pos[1])
677
robots[it1].ori = math.atan2(vect_temp[1], vect_temp[0])
678
robots[it1].key_neighbors = [it0, i]
679
# update the 'groups' variable
680
groups[g_it][0] = groups[g_it][0] + 1 # increase group size by 1
681
groups[g_it][3].append(i)
682
groups[g_it][4] = groups[g_it][4] + life_incre
683
# 3.s_forming1, robot '0' forms a pair with another '0', both are beoming '1_0_0'
684
s_pairs = [] # the container for the finalized pairs
685
while len(s_forming1.keys()) != 0: # there are still robots to be processed
686
for i in s_forming1.keys():
687
it = s_forming1[i][0] # index temp
688
if it in s_forming1.keys():
689
if s_forming1[it][0] == i: # robot 'it' also recognizes 'i' as closest
690
s_pairs.append([i, it])
691
s_forming1.pop(i) # pop out both 'i' and 'it'
692
s_forming1.pop(it)
693
break
694
elif i not in s_forming1[it]:
695
# usually should not be here unless robots have different sensing range
696
s_forming1[i].remove(it)
697
if len(s_forming1[i]) == 0:
698
s_forming1.pop(i)
699
break
700
# have not considered the situation that 'i' in 'it' but not first one
701
else:
702
# will be here if robot 'it' is to be bounced away by '1', or merge with '2'
703
s_forming1[i].remove(it)
704
if len(s_forming1[i]) == 0:
705
s_forming1.pop(i)
706
break
707
# process the finalized pairs
708
for pair in s_pairs:
709
it0 = pair[0] # get indices of the pair
710
it1 = pair[1]
711
g_it = random.randint(0, group_id_upper_limit)
712
while g_it in groups.keys(): # keep generate until not duplicate
713
g_it = random.randint(0, group_id_upper_limit)
714
# update the 'robots' variable
715
robots[it0].status = 1
716
robots[it1].status = 1
717
robots[it0].status_1_sub = 0
718
robots[it1].status_1_sub = 0
719
robots[it0].status_1_0_sub = 0
720
robots[it1].status_1_0_sub = 0
721
robots[it0].group_id = g_it
722
robots[it1].group_id = g_it
723
robots[it0].key_neighbors = [it1]
724
robots[it1].key_neighbors = [it0]
725
vect_temp = (robots[it1].pos[0]-robots[it0].pos[0],
726
robots[it1].pos[1]-robots[it0].pos[1]) # from it0 to it1
727
ori_temp = math.atan2(vect_temp[1], vect_temp[0])
728
if dist_table[it0][it1] > loop_space:
729
robots[it0].ori = ori_temp
730
robots[it1].ori = reset_radian(ori_temp + math.pi)
731
else:
732
robots[it0].ori = reset_radian(ori_temp + math.pi)
733
robots[it1].ori = ori_temp
734
# update the 'groups' variable
735
groups[g_it] = [2, 0, [], [it0, it1], 2*life_incre, False] # new entry
736
# 4.s_form_done, robots '1_0_1' finish triangle forming, all are becoming '2'
737
for g_it in s_form_done:
738
# get the three robots
739
it0 = groups[g_it][3][0]
740
it1 = groups[g_it][3][1]
741
it2 = groups[g_it][3][2]
742
# update the 'robots' variable
743
robots[it0].status = 2
744
robots[it1].status = 2
745
robots[it2].status = 2
746
robots[it0].vel = 0 # temporarily initialize with 0
747
robots[it1].vel = 0
748
robots[it2].vel = 0
749
robots[it0].status_2_avail2 = [True,True] # both sides of all are available
750
robots[it1].status_2_avail2 = [True,True]
751
robots[it2].status_2_avail2 = [True,True]
752
# update the 'groups' variable
753
groups[g_it][1] = 3 # three members on the loop now
754
groups[g_it][2] = [it0, it1, it2] # copy the same robots
755
groups[g_it][3] = [] # empty the temporary pool
756
# 5.s_merge_done, robot '1_1' finishes merging, becoming '2'
757
for i in s_merge_done:
758
g_it = robots[i].group_id # get the group id
759
it0 = robots[i].key_neighbors[0]
760
it1 = robots[i].key_neighbors[1]
761
# update for robot 'i'
762
robots[i].status = 2
763
robots[i].vel = 0
764
robots[i].status_2_avail2 = [True,True]
765
# update for robot 'it0' and 'it1'
766
robots[it0].key_neighbors[1] = i # update right side of left neighbor
767
robots[it1].key_neighbors[0] = i # update left side of right neighbor
768
robots[it0].status_2_avail2[1] = True
769
robots[it1].status_2_avail2[0] = True
770
# update for 'groups' variable
771
groups[g_it][1] = groups[g_it][1] + 1 # update number of robots on the loop
772
groups[g_it][2].append(i) # add to the list of robots on the loop
773
groups[g_it][3].remove(i) # remove from the list of robots off the loop
774
# 6.s_group_exp, natural life expiration of the groups
775
for g_it in s_group_exp:
776
s_disassemble.append([g_it]) # disassemble together in s_disassemble
777
# 7.s_disassemble, compare groups and disassemble all but the largest
778
s_dis_list = [] # list of groups that have been finalized for disassembling
779
# compare number of members to decide which groups to disassemble
780
for gs_it in s_disassemble:
781
if len(gs_it) == 1: # disassemble trigger from other sources
782
if gs_it[0] not in s_dis_list:
783
s_dis_list.append(gs_it[0])
784
else:
785
# compare which group has the most members, and disassemble the rest
786
g_temp = gs_it[:]
787
member_max = 0 # variable for maximum number of members
788
group_max = -1 # corresponding group id with member_max
789
for g_it in g_temp:
790
if groups[g_it][0] > member_max:
791
member_max = groups[g_it][0]
792
group_max = g_it
793
g_temp.remove(group_max) # remove the group with the most members
794
for g_it in g_temp:
795
if g_it not in s_dis_list:
796
s_dis_list.append(g_it)
797
# start disassembling
798
for g_it in s_dis_list:
799
# update the 'robots' variable
800
for i in groups[g_it][3]: # for robots off the line
801
robots[i].vel = const_vel # restore the faster speed
802
robots[i].status = -1
803
robots[i].status_n1_life = random.randint(n1_life_lower, n1_life_upper)
804
robots[i].ori = random.random() * 2*math.pi - math.pi
805
# continue the idea of exploding robots, but it's much more straightforward here
806
for i in groups[g_it][2]: # for robots on the loop
807
robots[i].vel = const_vel
808
robots[i].status = -1
809
robots[i].status_n1_life = random.randint(n1_life_lower, n1_life_upper)
810
it0 = robots[i].key_neighbors[0]
811
it1 = robots[i].key_neighbors[1]
812
vect_0 = (robots[it0].pos[0]-robots[it1].pos[0],
813
robots[it0].pos[1]-robots[it1].pos[1]) # from it1 to it0
814
vect_1 = (-vect_0[1], vect_0[0]) # rotate vect_0 90 degrees ccw
815
robots[i].ori = math.atan2(vect_1[1], vect_1[0]) # exploding orientation
816
# update 'groups' variable
817
groups.pop(g_it)
818
# 8.s_back_0, life time of robot '-1' expires, becoming '0'
819
for i in s_back_0:
820
# still maintaining the old moving direction and velocity
821
robots[i].status = 0
822
823
# update the physics(pos, vel, and ori), and wall bouncing, life decrease of '-1'
824
for i in range(robot_quantity):
825
# check if out of boundaries, same algorithm from previous line formation program
826
# change only direction of velocity
827
if robots[i].pos[0] >= world_size[0]: # out of right boundary
828
if math.cos(robots[i].ori) > 0: # velocity on x is pointing right
829
robots[i].ori = reset_radian(2*(math.pi/2) - robots[i].ori)
830
elif robots[i].pos[0] <= 0: # out of left boundary
831
if math.cos(robots[i].ori) < 0: # velocity on x is pointing left
832
robots[i].ori = reset_radian(2*(math.pi/2) - robots[i].ori)
833
if robots[i].pos[1] >= world_size[1]: # out of top boundary
834
if math.sin(robots[i].ori) > 0: # velocity on y is pointing up
835
robots[i].ori = reset_radian(2*(0) - robots[i].ori)
836
elif robots[i].pos[1] <= 0: # out of bottom boundary
837
if math.sin(robots[i].ori) < 0: # velocity on y is pointing down
838
robots[i].ori = reset_radian(2*(0) - robots[i].ori)
839
# update one step of distance
840
travel_dist = robots[i].vel * frame_period/1000.0
841
robots[i].pos[0] = robots[i].pos[0] + travel_dist*math.cos(robots[i].ori)
842
robots[i].pos[1] = robots[i].pos[1] + travel_dist*math.sin(robots[i].ori)
843
# update moving direction and destination of robot '1'
844
if robots[i].status == 1:
845
if robots[i].status_1_sub == 0 and robots[i].status_1_0_sub == 0:
846
# for the pair forming robots '1_0_0'
847
it0 = robots[i].key_neighbors[0]
848
if abs(dist_table[i][it0] - loop_space) < space_error:
849
# stop the robot if distance is good
850
robots[i].vel = 0
851
else:
852
vect_temp = (robots[it0].pos[0]-robots[i].pos[0],
853
robots[it0].pos[1]-robots[i].pos[1]) # from i to it0
854
ori_temp = math.atan2(vect_temp[1], vect_temp[0])
855
# update the moving direction
856
if dist_table[i][it0] > loop_space:
857
robots[i].ori = ori_temp
858
else:
859
robots[i].ori = reset_radian(ori_temp + math.pi)
860
elif ((robots[i].status_1_sub == 0 and robots[i].status_1_0_sub == 1) or
861
robots[i].status_1_sub == 1):
862
# for the triangle forming robots '1_0_1', or merging robots '1_1'
863
it0 = robots[i].key_neighbors[0]
864
it1 = robots[i].key_neighbors[1]
865
new_des = des_solver(robots[it0].pos, robots[it1].pos,
866
dist_table[it0][it1], loop_space)
867
# update the new destination
868
if robots[i].status_1_sub == 0: # for '1_0_1'
869
robots[i].status_1_0_1_des = new_des
870
else: # for '1_1'
871
robots[i].status_1_1_des = new_des
872
vect_temp = (new_des[0]-robots[i].pos[0],
873
new_des[1]-robots[i].pos[1])
874
robots[i].ori = math.atan2(vect_temp[1], vect_temp[0])
875
# update moving direction, velocity and first merge availability of robot '2'
876
elif robots[i].status == 2:
877
it0 = robots[i].key_neighbors[0]
878
it1 = robots[i].key_neighbors[1]
879
vect_0 = (robots[it0].pos[0]-robots[i].pos[0],
880
robots[it0].pos[1]-robots[i].pos[1]) # from i to it0
881
vect_1 = (robots[it1].pos[0]-robots[i].pos[0],
882
robots[it1].pos[1]-robots[i].pos[1]) # from i to it1
883
ori_0 = math.atan2(vect_0[1], vect_0[0])
884
ori_1 = math.atan2(vect_1[1], vect_1[0])
885
# calculate the interior angle of the polygon at robot 'i'
886
angle_inter = ori_0 - ori_1 # angle from vect_1 to vect_0
887
# reset to pisitive angle, do not use reset_radian() here
888
if angle_inter < 0: angle_inter = angle_inter + 2*math.pi
889
# update the first merge availability
890
if angle_inter > math.pi:
891
# only set to false if exceed the upper limit, not the lower limit
892
robots[i].status_2_avail1 = False
893
else:
894
robots[i].status_2_avail1 = True
895
# update moving direction and velocity
896
# also keep away from same group robots '2' that are not key neighbors
897
new_des = des_solver(robots[it0].pos, robots[it1].pos,
898
dist_table[it0][it1], loop_space)
899
# update moving orientation and velocity based on calculated new destination
900
vect_temp = (new_des[0]-robots[i].pos[0],
901
new_des[1]-robots[i].pos[1])
902
robots[i].ori = math.atan2(vect_temp[1], vect_temp[0])
903
dist_temp = math.sqrt(vect_temp[0]*vect_temp[0]+
904
vect_temp[1]*vect_temp[1])
905
robots[i].vel = dist_temp * adjust_vel_coef
906
if len(index_list[i]) > 0:
907
# there are same group non-neighbor robots '2' around
908
# find the closest one within them, and move away from it
909
dist_min = 2*comm_range
910
robot_min = -1
911
for j in index_list[i]:
912
if dist_table[i][j] < dist_min and dist_table[i][j] > 0:
913
dist_min = dist_table[i][j]
914
robot_min = j
915
# robot_min is the acquired closest robot
916
vect_temp = (robots[i].pos[0]-robots[robot_min].pos[0],
917
robots[i].pos[1]-robots[robot_min].pos[1])
918
robots[i].ori = math.atan2(vect_temp[1], vect_temp[0])
919
# not updating velocity here, use the same urgency of moving to ideal des
920
# decrease life time of robot '-1'
921
elif robots[i].status == -1:
922
robots[i].status_n1_life = robots[i].status_n1_life - frame_period/1000.0
923
# life time decrease of the groups
924
for g_it in groups.keys():
925
if groups[g_it][5]: continue # skip the dominant group
926
groups[g_it][4] = groups[g_it][4] - frame_period/1000.0
927
928
# graphics update
929
screen.fill(background_color)
930
# draw the robots
931
for i in range(robot_quantity):
932
display_pos = world_to_display(robots[i].pos, world_size, screen_size)
933
# get color of the robot
934
color_temp = ()
935
if robots[i].status == 0:
936
color_temp = robot_0_color
937
elif robots[i].status == 1:
938
if robots[i].status_1_sub == 0:
939
if robots[i].status_1_0_sub == 0:
940
color_temp = robot_1_0_0_color
941
elif robots[i].status_1_0_sub == 1:
942
color_temp = robot_1_0_1_color
943
elif robots[i].status_1_sub == 1:
944
color_temp = robot_1_1_color
945
elif robots[i].status == 2:
946
color_temp = robot_2_color
947
elif robots[i].status == -1:
948
color_temp = robot_n1_color
949
# draw the robot as a small solid circle
950
pygame.draw.circle(screen, color_temp, display_pos, robot_size, 0) # fill the circle
951
# draw the line segments for the loops
952
for g_it in groups.keys():
953
if groups[g_it][1] > 0:
954
# this group is not in the initial forming phase
955
# will not use the groups[g_it][2] to draw the line, it's not ordered
956
# start with first robot in groups[g_it][2], and use key neighbors to continue
957
# this also acts as a way to check if key_neighbors are in good shape
958
it_start = groups[g_it][2][0]
959
# do one iteration first
960
it0 = it_start
961
it1 = robots[it0].key_neighbors[1] # going ccw
962
it0_disp = world_to_display(robots[it0].pos, world_size, screen_size)
963
it1_disp = world_to_display(robots[it1].pos, world_size, screen_size)
964
pygame.draw.line(screen, robot_2_color, it0_disp, it1_disp)
965
line_count = 1 # count number of line segments that have been drawn
966
while it1 != it_start: # keep iterating
967
it0 = it1
968
it1 = robots[it0].key_neighbors[1]
969
it0_disp = world_to_display(robots[it0].pos, world_size, screen_size)
970
it1_disp = world_to_display(robots[it1].pos, world_size, screen_size)
971
pygame.draw.line(screen, robot_2_color, it0_disp, it1_disp)
972
line_count = line_count + 1
973
# check if number of drawed line segments is correct
974
if line_count != groups[g_it][1]:
975
print "group {} has {} nodes but {} line segments".format(g_it, groups[g_it][1],
976
line_count)
977
pygame.display.update()
978
979
pygame.quit()
980
981
982
983