Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
yangliu28
GitHub Repository: yangliu28/swarm_formation_sim
Path: blob/master/loop_reshape_2_dynamic.py
104 views
1
# this is an algorithm revision besed on 'loop_reshape_1_static.py'
2
# First section is copied verbatim, algorithm tests are carried on in second section for the
3
# preferability distribution evolution.
4
5
# arguments format:
6
# '-i': filename of the initial loop formation; default is generate the initial formation
7
# '-t': filename of the target loop formation; default is generate the target formation
8
# '--gensave': option to all or none of the generated formations; default is discard
9
# '--nobargraph': option to skip the bar graph visualization; default is with bargraph
10
11
# Revised algorithm in the second section:
12
# New algorithm combines weighted averaging, linear multiplier and power function methods.
13
# Since equal weighted averaging method can guarantee convergence(although unipolarity of
14
# resulting distribution can be very poor), a revised weighted averaging method was
15
# implemented here to ensure as much convergence as possible. Each node is given a new
16
# piece of information, that is how many nodes in the adjacent block agree on which target
17
# node they should be. (This adjacent block is referred as a group.) This information
18
# will help to resolve any conflict on the boundary of two groups. And ideally, the
19
# group with higher number of nodes should win. With this conflict resolving mechanism,
20
# the linear multiplier method is used to improve the unipolarity of the distributions. This
21
# method has similar effect of previous power function method with bigger than 1 exponent,
22
# but in a slower and constant growing rate. The linear multiplier is only used when two
23
# neighbors converges with the host on the formation. How much the unipolarity should grow
24
# depends on the largest difference of distributions when using averaging method. The larger
25
# the difference, the less the unipolarity should grow. A power function method with exponent
26
# smaller than 1 is used to slow down the increasing rate of the unipolarity.
27
28
# SMA motion control for the loop reshape process:
29
# Inspired by the shape memory alloy, each node maintain desired loop space by modeling the
30
# feedback from neighbors as stiff linear springs. Each node also trys to achived a certain
31
# interior angle, the difference from current interior angle to target interior angle acts
32
# as a potential energy, just like the memorized shape of the alloy. The node trys to
33
# accomodate this effect by moving itself along the central axis, the effect can be exerted
34
# from two neighbors, but they may have conflicting ideas, so it is simplier to just move
35
# host node along central axis. This feedback is also modeled as a rotating spring. This two
36
# combined spring force can reshape the loop to desired loop formation.
37
38
# 01/29/2018
39
# Adding colors to simulation after testing it with consensus algorithm simulation.
40
# An exhibited decision for a group is necessary to assign the colors. There was no such
41
# concept before, so the exhibited decision is defined as first node's choice.
42
43
# 02/15/2018
44
# Add the square and circle as the targets for loop reshape.
45
46
47
import pygame
48
from formation_functions import *
49
import matplotlib.pyplot as plt
50
from matplotlib import gridspec
51
52
import sys, os, math, random, time, getopt
53
import numpy as np
54
55
# read simulation options from arguments
56
form_opts = [0,0] # indicating where the initial and target formation comes from
57
# '0' for randomly generated
58
# '1' for read from file
59
form_files = [0,0] # filenames for the formations if read from file
60
gen_save = False # whether saving all or none generated formations
61
show_bargraph = True # whether showing bargraphs of the preference distributions
62
try:
63
opts, args = getopt.getopt(sys.argv[1:], 'i:t:', ['gensave','nobargraph'])
64
except getopt.GetoptError as err:
65
print str(err)
66
sys.exit()
67
for opt,arg in opts:
68
if opt == '-i':
69
# read initial formation from file
70
form_opts[0] = 1
71
form_files[0] = arg # get the filename of the initial formation
72
elif opt == '-t':
73
# read target formation from file
74
form_opts[1] = 1
75
form_files[1] = arg # get the filename of the target formation
76
elif opt == '--gensave':
77
# save any generated formations
78
gen_save = True
79
elif opt == '--nobargraph':
80
show_bargraph = False
81
82
########################### start of section 1 ###########################
83
84
# initialize the pygame
85
pygame.init()
86
87
# name of the folder under save directory that stores loop formation files
88
loop_folder = 'loop-data'
89
90
# parameters for display, window origin is at left up corner
91
# screen_size: width and height in pixels
92
screen_size = (600, 800) # for network size of 30
93
# screen_size = (900, 1200) # for network size of 100
94
# top half for initial formation, bottom half for target formation
95
color_black = (0,0,0)
96
color_white = (255,255,255)
97
# a set of 20 distinct colors (black and white excluded)
98
distinct_color_set = ((230,25,75), (60,180,75), (255,225,25), (0,130,200), (245,130,48),
99
(145,30,180), (70,240,240), (240,50,230), (210,245,60), (250,190,190),
100
(0,128,128), (230,190,255), (170,110,40), (255,250,200), (128,0,0),
101
(170,255,195), (128,128,0), (255,215,180), (0,0,128), (128,128,128))
102
robot_size = 7 # robot modeled as dot, number of pixels for radius
103
group_line_wid = 4 # width of the connecting lines in the group
104
105
# set up the simulation window and surface object
106
icon = pygame.image.load("icon_geometry_art.jpg")
107
pygame.display.set_icon(icon)
108
screen = pygame.display.set_mode(screen_size)
109
pygame.display.set_caption("Loop Reshape 2 (dynamic version)")
110
111
# for physics, continuous world, origin is at left bottom corner, starting (0, 0),
112
# with x axis pointing right, y axis pointing up.
113
# It's more natural to compute the physics in right hand coordiante system.
114
world_size = (100.0, 100.0 * screen_size[1]/screen_size[0])
115
116
# variables to configure the simulation
117
# poly_n: number of nodes for the polygon, also the robot quantity, at least 3
118
# loop_space: side length of the equilateral polygon
119
poly_n = 30 # for network size of 30
120
loop_space = 4.0 # for network size of 30
121
# poly_n = 100 # for network size of 100
122
# loop_space = 2.0 # for network size of 100
123
# desired loop space is a little over half of communication range
124
comm_range = loop_space/0.6
125
# upper and lower limits have equal difference to the desired loop space
126
space_upper = comm_range*0.9 # close but not equal to whole communication range
127
space_lower = comm_range*0.3
128
# ratio of speed to the distance difference
129
vel_dist_ratio = 1.0
130
# period for calculating new positions
131
# updating is not in real time, because bar graph animation takes too much time
132
physics_period = 500/1000.0 # second
133
# for the guessing of the free interior angles
134
int_angle_reg = math.pi - 2*math.pi/poly_n # interior angle of regular polygon
135
# standard deviation of the normal distribution of the guesses
136
int_angle_dev = (int_angle_reg - math.pi/3)/5
137
138
# instantiate the node positions variable
139
nodes = [[],[]] # node positions for two formation, index is the robot's identification
140
nodes[0].append([0, 0]) # first node starts at origin
141
nodes[0].append([loop_space, 0]) # second node is loop space away on the right
142
nodes[1].append([0, 0])
143
nodes[1].append([loop_space, 0])
144
for i in range(2, poly_n):
145
nodes[0].append([0, 0]) # filled with [0,0]
146
nodes[1].append([0, 0])
147
148
# construct the formation data for the two formation, either generating or from file
149
for i in range(2):
150
if form_opts[i] == 0: # option to generate a new formation
151
# process for generating the random equilateral polygon, two stages
152
poly_success = False # flag for succeed in generating the polygon
153
trial_count = 0 # record number of trials until a successful polygon is achieved
154
int_final = [] # interior angles to be saved later in file
155
while not poly_success:
156
trial_count = trial_count + 1
157
# print "trial {}: ".format(trial_count),
158
# continue trying until all the guesses can forming the desired polygon
159
# stage 1: guessing all the free interior angles
160
dof = poly_n-3 # number of free interior angles to be randomly generated
161
if dof > 0: # only continue guessing if at least one free interior angle
162
# generate all the guesses from a normal distribution
163
int_guesses = np.random.normal(int_angle_reg, int_angle_dev, dof).tolist()
164
ori_current = 0 # orientation of the line segment
165
no_irregular = True # flag indicating if the polygon is irregular or not
166
# example for irregular cases are intersecting of line segments
167
# or non neighbor nodes are closer than the loop space
168
# construct the polygon based on these guessed angles
169
for j in range(2, 2+dof): # for the position of j-th node
170
int_angle_t = int_guesses[j-2] # interior angle of previous node
171
ori_current = reset_radian(ori_current + (math.pi - int_angle_t))
172
nodes[i][j][0] = nodes[i][j-1][0] + loop_space*math.cos(ori_current)
173
nodes[i][j][1] = nodes[i][j-1][1] + loop_space*math.sin(ori_current)
174
# check the distance of node j to all previous nodes
175
# should not be closer than the loop space
176
for k in range(j-1): # exclude the previous neighbor
177
vect_temp = [nodes[i][k][0]-nodes[i][j][0],
178
nodes[i][k][1]-nodes[i][j][1]] # from j to k
179
dist_temp = math.sqrt(vect_temp[0]*vect_temp[0]+
180
vect_temp[1]*vect_temp[1])
181
if dist_temp < loop_space:
182
no_irregular = False
183
# print "node {} is too close to node {}".format(j, k)
184
break
185
if not no_irregular:
186
break
187
if not no_irregular:
188
continue # continue the while loop, keep trying new polygon
189
else: # if here, current interior angle guesses are good
190
int_final = int_guesses[:]
191
# although later check on the final node may still disqualify
192
# these guesses, the while loop will exit with a good int_final
193
# stage 2: use convex triangle for the rest, and deciding if polygon is possible
194
# solve the one last node
195
vect_temp = [nodes[i][0][0]-nodes[i][poly_n-2][0],
196
nodes[i][0][1]-nodes[i][poly_n-2][1]] # from n-2 to 0
197
dist_temp = math.sqrt(vect_temp[0]*vect_temp[0]+
198
vect_temp[1]*vect_temp[1])
199
# check distance between node n-2 and 0 to see if a convex triangle is possible
200
# the situation that whether node n-2 and 0 are too close has been excluded
201
if dist_temp > 2*loop_space:
202
# print("second last node is too far away from the starting node")
203
continue
204
else:
205
# calculate the position of the last node
206
midpoint = [(nodes[i][poly_n-2][0]+nodes[i][0][0])/2,
207
(nodes[i][poly_n-2][1]+nodes[i][0][1])/2]
208
perp_dist = math.sqrt(loop_space*loop_space - dist_temp*dist_temp/4)
209
perp_ori = math.atan2(vect_temp[1], vect_temp[0]) - math.pi/2
210
nodes[i][poly_n-1][0] = midpoint[0] + perp_dist*math.cos(perp_ori)
211
nodes[i][poly_n-1][1] = midpoint[1] + perp_dist*math.sin(perp_ori)
212
# also check any irregularity for the last node
213
no_irregular = True
214
for j in range(1, poly_n-2): # exclude starting node and second last node
215
vect_temp = [nodes[i][j][0]-nodes[i][poly_n-1][0],
216
nodes[i][j][1]-nodes[i][poly_n-1][1]] # from n-1 to j
217
dist_temp = math.sqrt(vect_temp[0]*vect_temp[0]+
218
vect_temp[1]*vect_temp[1])
219
if dist_temp < loop_space:
220
no_irregular = False
221
# print "last node is too close to node {}".format(j)
222
break
223
if no_irregular:
224
poly_success = True # reverse the flag
225
if i == 0: # for print message
226
print "initial formation generated at trial {}".format(trial_count)
227
else:
228
print "target formation generated at trial {}".format(trial_count)
229
# print("successful!")
230
# if here, a polygon has been successfully generated, save any new formation
231
if not gen_save: continue # skip following if option is not to save it
232
new_filename = get_date_time()
233
new_filepath = os.path.join(os.getcwd(), loop_folder, new_filename)
234
if os.path.isfile(new_filepath):
235
new_filename = new_filename + '-(1)' # add a suffix to avoid overwrite
236
new_filepath = new_filepath + '-(1)'
237
f = open(new_filepath, 'w')
238
f.write(str(poly_n) + '\n') # first line is the number of sides of the polygon
239
for j in int_final: # only recorded guessed interior angles
240
f.write(str(j) + '\n') # convert float to string
241
f.close()
242
# message for a file has been saved
243
if i == 0:
244
print('initial formation saved as "' + new_filename + '"')
245
else:
246
print('target formation saved as "' + new_filename + '"')
247
else: # option to read formation from file
248
new_filepath = os.path.join(os.getcwd(), loop_folder, form_files[i])
249
f = open(new_filepath, 'r') # read only
250
# check if the loop has the same number of side
251
if int(f.readline()) == poly_n:
252
# continue getting the interior angles
253
int_angles = []
254
new_line = f.readline()
255
while len(new_line) != 0: # not the end of the file yet
256
int_angles.append(float(new_line)) # add the new angle
257
new_line = f.readline()
258
# check if this file has the number of interior angles as it promised
259
if len(int_angles) != poly_n-3: # these many angles will determine the polygon
260
# the number of sides is not consistent inside the file
261
print 'file "{}" has incorrect number of interior angles'.format(form_files[i])
262
sys.exit()
263
# if here the data file is all fine, print message for this
264
if i == 0:
265
print 'initial formation read from file "{}"'.format(form_files[i])
266
else:
267
print 'target formation read from file "{}"'.format(form_files[i])
268
# construct the polygon from these interior angles
269
ori_current = 0 # orientation of current line segment
270
for j in range(2, poly_n-1):
271
int_angle_t = int_angles[j-2] # interior angle of previous node
272
ori_current = reset_radian(ori_current + (math.pi - int_angle_t))
273
nodes[i][j][0] = nodes[i][j-1][0] + loop_space*math.cos(ori_current)
274
nodes[i][j][1] = nodes[i][j-1][1] + loop_space*math.sin(ori_current)
275
# no need to check any irregularities
276
vect_temp = [nodes[i][0][0]-nodes[i][poly_n-2][0],
277
nodes[i][0][1]-nodes[i][poly_n-2][1]] # from node n-2 to 0
278
dist_temp = math.sqrt(vect_temp[0]*vect_temp[0]+
279
vect_temp[1]*vect_temp[1])
280
midpoint = [(nodes[i][poly_n-2][0]+nodes[i][0][0])/2,
281
(nodes[i][poly_n-2][1]+nodes[i][0][1])/2]
282
perp_dist = math.sqrt(loop_space*loop_space - dist_temp*dist_temp/4)
283
perp_ori = math.atan2(vect_temp[1], vect_temp[0]) - math.pi/2
284
nodes[i][poly_n-1][0] = midpoint[0] + perp_dist*math.cos(perp_ori)
285
nodes[i][poly_n-1][1] = midpoint[1] + perp_dist*math.sin(perp_ori)
286
else:
287
# the number of sides is not the same with poly_n specified here
288
print 'file "{}" has incorrect number of sides of polygon'.format(form_files[i])
289
sys.exit()
290
291
# shift the two polygon to the top and bottom halves
292
nodes = np.array(nodes) # convert to numpy array
293
for i in range(2):
294
# calculate the geometry center of current polygon
295
geometry_center = np.mean(nodes[i], axis=0)
296
# shift the polygon to the top or bottom half of the screen
297
nodes[i,:,0] = nodes[i,:,0] - geometry_center[0] + world_size[0]/2
298
if i == 0: # initial formation shift to top half
299
nodes[i,:,1] = nodes[i,:,1] - geometry_center[1] + 3*world_size[1]/4
300
else: # target formation shift to bottom half
301
nodes[i,:,1] = nodes[i,:,1] - geometry_center[1] + world_size[1]/4
302
303
# draw the two polygons
304
screen.fill(color_white)
305
for i in range(2):
306
# draw the nodes and line segments
307
disp_pos = [[0,0] for j in range(poly_n)]
308
# calculate the display pos for all nodes, draw them as red dots
309
for j in range(0, poly_n):
310
disp_pos[j] = world_to_display(nodes[i][j], world_size, screen_size)
311
pygame.draw.circle(screen, color_black, disp_pos[j], robot_size, 0)
312
# draw an outer circle to mark the starting node
313
pygame.draw.circle(screen, color_black, disp_pos[0], int(robot_size*1.5), 1)
314
# line segments for connecitons on the loop
315
for j in range(poly_n-1):
316
pygame.draw.line(screen, color_black, disp_pos[j], disp_pos[j+1])
317
pygame.draw.line(screen, color_black, disp_pos[poly_n-1], disp_pos[0])
318
pygame.display.update()
319
320
# raw_input("<Press Enter to continue>") # halt the program to check the networks
321
322
########################### start of section 2 ###########################
323
324
# calculate the interior angles of the two formations
325
# It's not necessary to do the calculation again, but may have this part ready
326
# for the dynamic version of the program for the loop reshape simulation.
327
inter_ang = [[0 for j in range(poly_n)] for i in range(2)]
328
for i in range(2):
329
for j in range(poly_n):
330
# for the interior angles of initial setup formation
331
node_h = nodes[i][j] # host node
332
node_l = nodes[i][(j-1)%poly_n] # node on the left
333
node_r = nodes[i][(j+1)%poly_n] # node on the right
334
vect_l = [node_l[0]-node_h[0], node_l[1]-node_h[1]] # from host to left
335
vect_r = [node_r[0]-node_h[0], node_r[1]-node_h[1]] # from host to right
336
# get the angle rotating from vect_r to vect_l
337
# # temporary fix for square formation
338
# try:
339
# inter_ang[i][j] = math.acos((vect_l[0]*vect_r[0] + vect_l[1]*vect_r[1])/
340
# (loop_space*loop_space))
341
# except:
342
# inter_ang[i][j] = math.acos(-1.0)
343
inter_ang[i][j] = math.acos((vect_l[0]*vect_r[0] + vect_l[1]*vect_r[1])/
344
(loop_space*loop_space))
345
if (vect_r[0]*vect_l[1] - vect_r[1]*vect_l[0]) < 0:
346
# cross product of vect_r to vect_l is smaller than 0
347
inter_ang[i][j] = 2*math.pi - inter_ang[i][j]
348
# the result interior angles should be in range of [0, 2*pi)
349
350
# rename the interior angle variables to be used later
351
# use interior angle instead of deviation angle because they should be equivalent
352
inter_curr = inter_ang[0][:] # interior angles of initial(dynamic) setup formation
353
inter_targ = inter_ang[1][:] # interior angles of target formation
354
# variable for the preferability distribution
355
pref_dist = np.zeros((poly_n, poly_n))
356
# variable indicating which target node has largest probability in the distributions
357
# this also represents which node it mostly prefers
358
domi_node = [0 for i in range(poly_n)] # dominant node for the distributions
359
# divide nodes on loop to groups based on dominant node
360
# only adjacent block of nodes are in same group if they agree on dominant node
361
groups = [] # lists of adjacent nodes inside
362
# variable indicating how many nodes are there in the same group with host node
363
group_sizes = [0 for i in range(poly_n)] # size of the group the node is in
364
color_initialized = False # whether the color assignment has run the first time
365
deci_colors = [-1 for i in range(poly_n)] # color index for each exhibited decision
366
# -1 for not assigned
367
color_assigns = [0 for i in range(20)] # number of assignments for each color
368
group_colors = [] # color for the groups
369
node_colors = [0 for i in range(poly_n)] # color for the nodes
370
# overflow threshold for the distribution difference
371
dist_diff_thres = 0.3
372
# variable for ratio of distribution difference to threshold, for tuning growing rate
373
# in range of [0,1], higher the ratio, slower it grows
374
dist_diff_ratio = [0 for i in range(poly_n)]
375
# exponent of the power function to map the ratio to a slower growing value
376
dist_diff_power = 0.3
377
# linear spring constant modeling the pulling and pushing effects of the neighbor nodes
378
linear_const = 1.0
379
# bending spring constant modeling the bending effect from host node itself
380
bend_const = 0.8
381
# disp_coef: coefficient from feedback vector to displacement
382
# disp_coef = 0.5 # for network size of 30
383
disp_coef = 0.2 # for network size of 100
384
385
# calculate the initial preferability distribution
386
for i in range(poly_n):
387
# the angle difference of inter_curr[i] to all angles in inter_targ
388
ang_diff = [0 for j in range(poly_n)]
389
ang_diff_sum = 0
390
for j in range(poly_n):
391
# angle difference represents the effort of change between two angles
392
# the more effort, the less the preferability, so take reciprocal
393
ang_diff[j] = 1/abs(inter_curr[i]-inter_targ[j])
394
# summation of ang_diff
395
ang_diff_sum = ang_diff_sum + ang_diff[j]
396
# convert to preferability distribution
397
for j in range(poly_n):
398
# linearize all probabilities such that sum(pref_dist[i])=1
399
pref_dist[i][j] = ang_diff[j]/ang_diff_sum
400
401
if show_bargraph:
402
# matplotlib method of bar graph animation
403
# adjust figure and grid size here
404
fig = plt.figure(figsize=(16,12), tight_layout=True)
405
fig.canvas.set_window_title('Evolution of Preferability Distribution')
406
gs = gridspec.GridSpec(5, 6)
407
ax = [fig.add_subplot(gs[i]) for i in range(poly_n)]
408
rects = [] # bar chart subplot rectangle handler
409
x_pos = range(poly_n)
410
for i in range(poly_n):
411
rects.append(ax[i].bar(x_pos, pref_dist[i], align='center'))
412
ax[i].set_xlim(-1, poly_n) # y limit depends on data set
413
414
# the loop
415
sim_exit = False # simulation exit flag
416
sim_pause = False # simulation pause flag
417
iter_count = 0
418
graph_iters = 1 # draw the distribution graphs every these many iterations
419
while not sim_exit:
420
# exit the program by close window button, or Esc or Q on keyboard
421
for event in pygame.event.get():
422
if event.type == pygame.QUIT:
423
sim_exit = True # exit with the close window button
424
if event.type == pygame.KEYUP:
425
if event.key == pygame.K_SPACE:
426
sim_pause = not sim_pause # reverse the pause flag
427
if (event.key == pygame.K_ESCAPE) or (event.key == pygame.K_q):
428
sim_exit = True # exit with ESC key or Q key
429
430
# skip the rest if paused
431
if sim_pause: continue
432
433
# prepare information for the preferability distribution evolution
434
# find the dominant node in each of the distributions
435
for i in range(poly_n):
436
domi_node_t = 0 # initialize the dominant node with the first one
437
domi_prob_t = pref_dist[i][0]
438
for j in range(1, poly_n):
439
if pref_dist[i][j] > domi_prob_t:
440
domi_node_t = j
441
domi_prob_t = pref_dist[i][j]
442
domi_node[i] = domi_node_t
443
# update the groups
444
groups = [[0]] # initialize with a node 0 robot
445
group_deci = [] # the exhibited decision of the groups
446
for i in range(1, poly_n):
447
if (domi_node[i-1]+1)%poly_n == domi_node[i]: # i-1 and i agree on dominant node
448
# simply add i to same group with i-1
449
groups[-1].append(i)
450
else:
451
# add a new group for node i in groups
452
groups.append([i])
453
# check if starting and ending robots should be in same groups
454
if (domi_node[poly_n-1]+1)%poly_n == domi_node[0] and len(groups)>1:
455
# add the first group to the last group
456
for i in groups[0]:
457
groups[-1].append(i)
458
groups.pop(0) # pop out the first group
459
# update group size
460
group_sizes = [0 for i in range(poly_n)] # initialize with all 0
461
for group in groups:
462
group_size_t = len(group)
463
for i in group:
464
group_sizes[i] = group_size_t
465
# update the exhibited decision for the groups
466
for i in range(len(groups)):
467
group_deci.append((domi_node[groups[i][0]] - groups[i][0]) % poly_n)
468
469
# update colors for the exhibited decisions
470
if not color_initialized:
471
color_initialized = True
472
select_set = range(20) # the initial selecting set
473
all_deci_set = set(group_deci) # put all exhibited decisions in a set
474
for deci in all_deci_set: # avoid checking duplicate decisions
475
if len(select_set) == 0:
476
select_set = range(20) # start a new set to select from
477
chosen_color = np.random.choice(select_set)
478
select_set.remove(chosen_color)
479
deci_colors[deci] = chosen_color # assign the chosen color to decision
480
# increase the assignments of chosen color by 1
481
color_assigns[chosen_color] = color_assigns[chosen_color] + 1
482
else:
483
# remove the color for a decision, if it's no longer the decision of any group
484
all_deci_set = set(group_deci)
485
for i in range(poly_n):
486
if deci_colors[i] != -1: # there was a color assigned before
487
if i not in all_deci_set:
488
# decrease the assignments of chosen color by 1
489
color_assigns[deci_colors[i]] = color_assigns[deci_colors[i]] - 1
490
deci_colors[i] = -1 # remove the assigned color
491
# assign color for an exhibited decision if not assigned
492
select_set = [] # set of colors to select from, start from empty
493
for i in range(len(groups)):
494
if deci_colors[group_deci[i]] == -1:
495
if len(select_set) == 0:
496
# construct a new select_set
497
color_assigns_min = min(color_assigns)
498
color_assigns_temp = [j - color_assigns_min for j in color_assigns]
499
select_set = range(20)
500
for j in range(20):
501
if color_assigns_temp[j] != 0:
502
select_set.remove(j)
503
# if here, the select_set is good to go
504
chosen_color = np.random.choice(select_set)
505
select_set.remove(chosen_color)
506
deci_colors[group_deci[i]] = chosen_color # assign the chosen color
507
# increase the assignments of chosen color by 1
508
color_assigns[chosen_color] = color_assigns[chosen_color] + 1
509
# update the color for the groups and nodes
510
group_colors = []
511
for i in range(len(groups)):
512
color_temp = deci_colors[group_deci[i]]
513
group_colors.append(color_temp) # color for the group
514
for node in groups[i]:
515
node_colors[node] = color_temp # color for the node
516
517
# preferability distribution evolution
518
pref_dist_t = np.copy(pref_dist) # deep copy the 'pref_dist', intermediate variable
519
for i in range(poly_n):
520
i_l = (i-1)%poly_n # index of neighbor on the left
521
i_r = (i+1)%poly_n # index of neighbor on the right
522
# shifted distribution from left neighbor
523
dist_l = [pref_dist_t[i_l][-1]] # first one copied from one at end
524
for j in range(poly_n-1):
525
dist_l.append(pref_dist_t[i_l][j])
526
# shifted distribution from right neighbor
527
dist_r = []
528
for j in range(1, poly_n):
529
dist_r.append(pref_dist_t[i_r][j])
530
dist_r.append(pref_dist_t[i_r][0]) # last one copied from one at starting
531
# calculating if two neighbors have converged ideas with host robot
532
converge_l = False
533
if (domi_node[i_l]+1)%poly_n == domi_node[i]: converge_l = True
534
converge_r = False
535
if (domi_node[i_r]-1)%poly_n == domi_node[i]: converge_r = True
536
# weighted averaging depending on group property
537
if converge_l and converge_r: # all three neighbors are in the same group
538
# step 1: take equal weighted average on all three distributions
539
dist_sum = 0
540
for j in range(poly_n):
541
pref_dist[i][j] = dist_l[j] + pref_dist_t[i][j] + dist_r[j]
542
dist_sum = dist_sum + pref_dist[i][j]
543
# linearize the distribution
544
pref_dist[i] = pref_dist[i]/dist_sum
545
# step 2: increase the unipolarity by applying the linear multiplier
546
# (step 2 is only for when both neighbors have converged opinions)
547
# first find the largest difference in two of the three distributions
548
dist_diff = [0, 0, 0] # variable for difference of three distribution
549
# distribution difference of left neighbor and host
550
for j in range(poly_n):
551
# difference of two distributions is sum of absolute individual differences
552
# use current step's distribution for distribution difference
553
dist_diff[0] = dist_diff[0] + abs(dist_l[j]-pref_dist_t[i][j])
554
# distribution difference of host and right neighbor
555
for j in range(poly_n):
556
dist_diff[1] = dist_diff[1] + abs(pref_dist_t[i][j]-dist_r[j])
557
# distribution difference of left and right neighbors
558
for j in range(poly_n):
559
dist_diff[2] = dist_diff[2] + abs(dist_l[j]-dist_r[j])
560
# maximum distribution differences
561
dist_diff_max = max(dist_diff)
562
if dist_diff_max < dist_diff_thres:
563
dist_diff_ratio[i] = dist_diff_max/dist_diff_thres # for debugging
564
# will skip step 2 if maximum difference is larger than the threshold
565
# linear multiplier is generated from the smallest and largest probabilities
566
# the smaller end is linearly mapped from largest distribution difference
567
# '1.0/poly_n' is the average of the linear multiplier
568
small_end = 1.0/poly_n * np.power(dist_diff_max/dist_diff_thres, dist_diff_power)
569
large_end = 2.0/poly_n - small_end
570
# sort the magnitude of processed distribution
571
dist_t = np.copy(pref_dist[i]) # temporary distribution
572
sort_index = range(poly_n)
573
for j in range(poly_n-1): # bubble sort, ascending order
574
for k in range(poly_n-1-j):
575
if dist_t[k] > dist_t[k+1]:
576
# exchange values in 'dist_t'
577
temp = dist_t[k]
578
dist_t[k] = dist_t[k+1]
579
dist_t[k+1] = temp
580
# exchange values in 'sort_index'
581
temp = sort_index[k]
582
sort_index[k] = sort_index[k+1]
583
sort_index[k+1] = temp
584
# applying the linear multiplier
585
dist_sum = 0
586
for j in range(poly_n):
587
multiplier = small_end + float(j)/(poly_n-1) * (large_end-small_end)
588
pref_dist[i][sort_index[j]] = pref_dist[i][sort_index[j]] * multiplier
589
dist_sum = dist_sum + pref_dist[i][sort_index[j]]
590
# linearize the distribution
591
pref_dist[i] = pref_dist[i]/dist_sum
592
else:
593
dist_diff_ratio[i] = 1.0 # for debugging, ratio overflowed
594
else: # at least one side has not converged yet
595
dist_diff_ratio[i] = -1.0 # indicating linear multiplier was not used
596
# take unequal weight in the averaging process based on group property
597
group_size_l = group_sizes[i_l]
598
group_size_r = group_sizes[i_r]
599
# taking the weighted average
600
dist_sum = 0
601
for j in range(poly_n):
602
# weight on left is group_size_l, on host is 1, on right is group_size_r
603
pref_dist[i][j] = (dist_l[j]*group_size_l + pref_dist[i][j] +
604
dist_r[j]*group_size_r)
605
dist_sum = dist_sum + pref_dist[i][j]
606
pref_dist[i] = pref_dist[i]/dist_sum
607
608
##### previous motion strategy #####
609
# # physics update, and carry out one iteration of position update
610
# for i in range(poly_n):
611
# node_h = nodes[0][i] # position of host node
612
# node_l = nodes[0][(i-1)%poly_n] # position of left neighbor
613
# node_r = nodes[0][(i+1)%poly_n] # position of right neighbor
614
# # find the central axis between the two neighbors
615
# pos_m = [(node_l[0]+node_r[0])/2, (node_l[1]+node_r[1])/2] # the origin on the axis
616
# vect_rl = [node_l[0]-node_r[0], node_l[1]-node_r[1]] # from node_r to node_l
617
# # distance of the two neighbors
618
# dist_rl = math.sqrt(vect_rl[0]*vect_rl[0]+vect_rl[1]*vect_rl[1])
619
# vect_rl = [vect_rl[0]/dist_rl, vect_rl[1]/dist_rl] # become unit vector
620
# vect_ax = [-vect_rl[1], vect_rl[0]] # central axis pointing outwords of the polygon
621
# vect_ax_ang = math.atan2(vect_ax[1], vect_ax[0])
622
# # all destinations will be defined as how much distance to pos_m along the axis
623
624
# # find the target destination that satisfies desired interior angle
625
# ang_targ = inter_targ[domi_node[i]] # dynamic target interior angle
626
# # distance of target position along the axis
627
# targ_dist = loop_space*math.cos(ang_targ/2)
628
# # reverse distance if interior angle is over pi
629
# if ang_targ > math.pi: targ_dist = -targ_dist
630
631
# # find the stable destination that satisfies the desired loop space
632
# # then decide the final destination by comparing with target destination
633
# final_dist = 0 # variable for the final destination
634
# # Following discussion is based on the range of dist_rl being divided into four
635
# # regions by three points, they are 2*space_upper, 2*loop_space, and 2*space_lower.
636
# if dist_rl >= 2*space_upper:
637
# # two neighbors are too far away, over the upper space limit the host can reach
638
# # no need to compare with target destination, ensure connection first
639
# final_dist = 0 # final destination is at origin
640
# elif dist_rl >= 2*loop_space and dist_rl < 2*space_upper:
641
# # the final destination has a tight single range, stable destination is at origin
642
# stab_dist = 0
643
# # calculate the half range for the final destination
644
# # the rangeis symmetric about the origin, lower range is negative of upper range
645
# range_upper = math.sqrt(space_upper*space_upper-dist_rl*dist_rl/4)
646
# # provisional final destination, balance between interior angle and loop space
647
# # final_dist = (targ_dist+stab_dist)/2
648
# final_dist = targ_dist*0.618 + stab_dist*0.382
649
# # set final destination to limiting positions if exceeding them
650
# final_dist = max(min(final_dist, range_upper), -range_upper)
651
# elif dist_rl >= 2*space_lower and dist_rl < 2*loop_space:
652
# # the final destination has only one range
653
# # but two stable destinations, will choose one closer to target destination
654
# stab_dist = math.sqrt(loop_space*loop_space-dist_rl*dist_rl/4)
655
# range_upper = math.sqrt(space_upper*space_upper-dist_rl*dist_rl/4)
656
# # check which stable destination the target destination is closer to
657
# if abs(targ_dist-stab_dist) < abs(targ_dist+stab_dist):
658
# # closer to stable destination at positive side
659
# # final_dist = (targ_dist+stab_dist)/2
660
# final_dist = targ_dist*0.618 + stab_dist*0.382
661
# else:
662
# # closer to stable destination at negative side
663
# # final_dist = (targ_dist-stab_dist)/2
664
# final_dist = targ_dist*0.618 + (-stab_dist)*0.382
665
# # set final destination to limiting positions if exceeding them
666
# final_dist = max(min(final_dist, range_upper), -range_upper)
667
# elif dist_rl < 2*space_lower:
668
# # final destination has two possible ranges to choose from
669
# # will take one on the side where the node is currently located
670
# stab_dist = math.sqrt(loop_space*loop_space-dist_rl*dist_rl/4)
671
# range_upper = math.sqrt(space_upper*space_upper-dist_rl*dist_rl/4)
672
# range_lower = math.sqrt(space_lower*space_lower-dist_rl*dist_rl/4)
673
# # find out the current side of the node
674
# vect_lr = [-vect_rl[0], -vect_rl[1]] # vector from left neighbor to right
675
# vect_lh = [node_h[0]-node_l[0], node_h[1]-node_l[1]] # from left to host
676
# # cross product of vect_lh and vect_lr
677
# if vect_lh[0]*vect_lr[1]-vect_lh[1]*vect_lr[0] >= 0:
678
# # host node is at positive side
679
# # final_dist = (targ_dist+stab_dist)/2
680
# final_dist = targ_dist*0.618 + stab_dist*0.382
681
# # avoid overflow in range [range_lower, range_upper]
682
# final_dist = max(min(final_dist, range_upper), range_lower)
683
# else:
684
# # host node is at negative side
685
# # final_dist = (targ_dist-stab_dist)/2
686
# final_dist = targ_dist*0.618 + (-stab_dist)*0.382
687
# # avoid overflow in range [-range_upper, -range_lower]
688
# final_dist = max(min(final_dist, -range_lower), -range_upper)
689
# # calculate the position of the final destination, where the node desires to move to
690
# final_des = [pos_m[0] + final_dist*math.cos(vect_ax_ang),
691
# pos_m[1] + final_dist*math.sin(vect_ax_ang)]
692
693
# # calculate the velocity and orientation based on the final destination
694
# vect_des = [final_des[0]-node_h[0], final_des[1]-node_h[1]] # from host to des
695
# vect_des_dist = math.sqrt(vect_des[0]*vect_des[0] + vect_des[1]*vect_des[1])
696
# # velocity is proportional to the distance to the final destination
697
# vel = vel_dist_ratio*vect_des_dist
698
# ori = math.atan2(vect_des[1], vect_des[0])
699
700
# # carry out one step of update on the position
701
# nodes[0][i] = [node_h[0] + vel*physics_period*math.cos(ori),
702
# node_h[1] + vel*physics_period*math.sin(ori)]
703
704
##### new SMA motion algorithm based on 'loop_reshape_test_moiton.py' #####
705
# variable for the neighboring nodes on the loop
706
dist_neigh = [0 for i in range(poly_n)]
707
# update the dynamic neighbor distances of the loop
708
for i in range(poly_n):
709
vect_r = nodes[0][(i+1)%poly_n] - nodes[0][i] # from host to right node
710
dist_neigh[i] = math.sqrt(vect_r[0]*vect_r[0] + vect_r[1]*vect_r[1])
711
# update the dynamic interior angles of the loop
712
for i in range(poly_n):
713
i_l = (i-1)%poly_n
714
i_r = (i+1)%poly_n
715
vect_l = nodes[0][i_l] - nodes[0][i]
716
vect_r = nodes[0][i_r] - nodes[0][i]
717
inter_curr[i] = math.acos((vect_l[0]*vect_r[0] + vect_l[1]*vect_r[1])/
718
(dist_neigh[i_l] * dist_neigh[i]))
719
if (vect_r[0]*vect_l[1] - vect_r[1]*vect_l[0]) < 0:
720
inter_curr[i] = 2*math.pi - inter_curr[i]
721
# the feedback from all spring effects
722
fb_vect = [np.zeros([1,2]) for i in range(poly_n)]
723
for i in range(poly_n):
724
i_l = (i-1)%poly_n
725
i_r = (i+1)%poly_n
726
# unit vector from host to left
727
vect_l = (nodes[0][i_l]-nodes[0][i])/dist_neigh[i_l]
728
# unit vector from host to right
729
vect_r = (nodes[0][i_r]-nodes[0][i])/dist_neigh[i]
730
# unit vector along central axis pointing inward the polygon
731
vect_lr = nodes[0][i_r]-nodes[0][i_l] # from left neighbor to right
732
dist_temp = math.sqrt(vect_lr[0]*vect_lr[0]+vect_lr[1]*vect_lr[1])
733
vect_in = np.array([-vect_lr[1]/dist_temp, vect_lr[0]/dist_temp]) # rotate ccw pi/2
734
735
# add the pulling or pushing effect from left neighbor
736
fb_vect[i] = fb_vect[i] + (dist_neigh[i_l]-loop_space) * linear_const * vect_l
737
# add the pulling or pushing effect from right neighbor
738
fb_vect[i] = fb_vect[i] + (dist_neigh[i]-loop_space) * linear_const * vect_r
739
# add the bending effect initialized by the host node itself
740
fb_vect[i] = fb_vect[i] + ((inter_targ[domi_node[i]]-inter_curr[i])*
741
bend_const * vect_in)
742
# update positions once, comment below if wishing to see the decision process only
743
nodes[0][i] = nodes[0][i] + disp_coef * fb_vect[i]
744
745
# use delay to slow down the physics update when bar graph animation is skpped
746
# not clean buy quick way to adjust simulation speed
747
time.sleep(0.2) # in second
748
749
# iteration count update
750
print("iteration count {}".format(iter_count))
751
iter_count = iter_count + 1 # update iteration count
752
753
# graphics update
754
if iter_count%graph_iters == 0:
755
756
if show_bargraph:
757
# graphics update for the bar graph
758
# find the largest y data in all distributions as up limit in graphs
759
y_lim = 0.0
760
for i in range(poly_n):
761
for j in range(poly_n):
762
if pref_dist[i][j] > y_lim:
763
y_lim = pref_dist[i][j]
764
y_lim = min(1.0, y_lim*1.1) # leave some gap
765
# matplotlib method
766
for i in range(poly_n):
767
for j in range(poly_n):
768
rects[i][j].set_height(pref_dist[i][j])
769
ax[i].set_title('{} -> {} -> {:.2f}'.format(i, group_sizes[i], dist_diff_ratio[i]))
770
ax[i].set_ylim(0.0, y_lim)
771
fig.canvas.draw()
772
fig.show()
773
774
# graphics update for the pygame window
775
screen.fill(color_white)
776
for i in range(2):
777
# calculate the display pos for all nodes
778
disp_pos = [[0,0] for j in range(poly_n)]
779
for j in range(0, poly_n):
780
disp_pos[j] = world_to_display(nodes[i][j], world_size, screen_size)
781
# draw the connecting lines
782
for j in range(poly_n-1):
783
pygame.draw.line(screen, color_black, disp_pos[j], disp_pos[j+1])
784
pygame.draw.line(screen, color_black, disp_pos[poly_n-1], disp_pos[0])
785
# draw the group connections and nodes for top and bottom formations
786
if i == 0: # for top formation
787
# highlight the group connections
788
for j in range(len(groups)):
789
for k in range(len(groups[j])-1):
790
pair_l = groups[j][k]
791
pair_r = groups[j][k+1]
792
pygame.draw.line(screen, distinct_color_set[group_colors[j]],
793
disp_pos[pair_l], disp_pos[pair_r], group_line_wid)
794
if len(groups) == 1:
795
# draw extra segment for starting and end node
796
pair_l = groups[0][-1]
797
pair_r = groups[0][0]
798
pygame.draw.line(screen, distinct_color_set[group_colors[0]],
799
disp_pos[pair_l], disp_pos[pair_r], group_line_wid)
800
# draw all nodes as their group colors
801
for j in range(poly_n):
802
pygame.draw.circle(screen, distinct_color_set[node_colors[j]],
803
disp_pos[j], robot_size, 0)
804
else: # for bottom formation
805
# draw all nodes as black dots
806
for j in range(poly_n):
807
pygame.draw.circle(screen, color_black, disp_pos[j], robot_size, 0)
808
# draw an outer circle to mark the starting node
809
pygame.draw.circle(screen, color_black, disp_pos[0], int(robot_size*1.5), 1)
810
pygame.display.update()
811
812
# hold the program to check the networks
813
# raw_input("<Press Enter to continue>")
814
815