# loop formation simulation of swarm robots, form a triangle, and build loop by merging12# comments for programming purpose:3# Similar status design with the second line formation:4# '0': free, wondering around, available to form a pair, a triangle or joint a group5# '1': transit status between '0' and '2', robot in a group, but not a loop yet6# '1_0': forming status, either in a pair, or in a triangle formation7# '1_0_0': forming status, in a pair8# '1_0_1': forming status, in a triangle9# '1_1': merging status, joining a robot '2' group10# '2': formal members of a loop formation group11# '-1': free, wondering around, ignoring all interactions12# Only allowed status transitions are:13# forward: '-1'->'0', '0'->'1_0_0', '0'->'1_0_1', '0'->'1_1', '1_0_0'->'1_0_1'14# '1_0_1'->'2', '1_1'->215# backward: '1'->'-1', '2'->'-1'16# Status transition regarding robot '1':17# When 2 '0' meets, they both become '1_0_0', and start adjust distance to the loop18# space. When distance is good, they don't perform any status transition yet. Only19# when a new '0' comes over, it will trigger all three robots into '1_0_1' status.20# And during the entire time of '1_0_0', the two robots are always welcoming new '0',21# even their distance is not good yet. When in '1_0_1', after the three robots form22# the perfect triangle and good loop space, they will trigger a status transition23# of becoming '2'.24# Two-stage forming explanation:25# It's likely to happen that a '0' meets two '0' at the same time, especially at the26# the begining of the simulation. It would be conveniently to form a triangle directly27# if this happens. But I stick with two stage forming, to have a pair before the triangle.28# Mainly because it makes programming simplier, and it takes another loop to turn the29# pair to a triangle. And also in real robots, the pair will be established before the30# triangle.3132# There is no index indicating where the robot is on the loop, that means each robot's33# role is equal. Each has two neighbors, one on left, one on right. There is no start or34# end on the loop.3536# A simple loop adjusting method was used. Each robot keeps moving to achieve the desired37# loop space. In the meantime, if a robot on the loop meets another robot on the same loop38# that is not one of its key neighbors, it moves away from that robot, using the old velocity39# for the loop space destination.404142import pygame43import math, random, sys44from loop_formation_robot import LFRobot45from formation_functions import *4647pygame.init()4849# for display, window origin is at left up corner50screen_size = (1200, 1000) # width and height51background_color = (0, 0, 0) # black background52robot_0_color = (0, 255, 0) # for robot status '0', green53robot_1_0_0_color = (255, 153, 153) # for robot status '1_0_0', light pink54robot_1_0_1_color = (255, 102, 102) # for robot status '1_0_1', dark pink55robot_1_1_color = (255, 102, 102) # for robot status '1_1', dark pink56robot_2_color = (255, 0, 0) # for robot status '2', red57robot_n1_color = (0, 0, 102) # for robot status '-1', blue58robot_size = 5 # robot modeled as dot, number of pixels in radius5960# set up the simulation window and surface object61icon = pygame.image.load('icon_geometry_art.jpg')62pygame.display.set_icon(icon)63screen = pygame.display.set_mode(screen_size)64pygame.display.set_caption('Loop Formation Simulation')6566# for physics, continuous world, origin is at left bottom corner, starting (0, 0),67# with x axis pointing right, y axis pointing up.68# It's more natural to compute the physics in right hand coordiante system.69world_size = (100.0, 100.0 * screen_size[1]/screen_size[0])7071# variables to configure the simulation72robot_quantity = 3073distrib_coef = 0.5 # coefficient to resize the initial robot distribution74const_vel = 4.0 # all robots except those in adjusting phase are moving at this speed75frame_period = 100 # updating period of the simulation and graphics, in millisecond76comm_range = 5.0 # sensing and communication share this same range, in radius77loop_space = comm_range * 0.75 # desired space of robots on the lop78space_error = loop_space * 0.2 # error to determine if a status transition is needed79life_incre = 10 # number of seconds a new member adds to a group80group_id_upper_limit = 1000 # upper limit of random integer for group id81n1_life_lower = 3 # lower limit of life time for status '-1'82n1_life_upper = 8 # upper limit of life time for status '-1'83# coefficient for calculating velocity of robot '2' on the loop for adjusting84# as the distance error decreases, the loop adjusting velocity also decreases85adjust_vel_coef = const_vel/loop_space * 0.88687# instantiate the robot swarm as list88robots = [] # container for all robots, index is its identification89for i in range(robot_quantity):90# random position, away from the window's edges91pos_temp = (((random.random()-0.5)*distrib_coef+0.5) * world_size[0],92((random.random()-0.5)*distrib_coef+0.5) * world_size[1])93vel_temp = const_vel94ori_temp = random.random() * 2*math.pi - math.pi # random in (-pi, pi)95object_temp = LFRobot(pos_temp, vel_temp, ori_temp)96robots.append(object_temp)97# instantiate the group variable as dictionary98groups = {}99# key is the group id100# value is a list101# 0.first element: the group size, all members including both '2' and '1'102# 1.second element: the number of robots on the loop, only robot '2'103# 2.third element: a list of robots on the loop, nor ordered, status '2'104# 3.fourth element: a list of robots off the loop, not ordered, status '1'105# 4.second element: remaining life time106# 5.fifth element: true or false of being the dominant group107# instantiate a distance table for every pair of robots108# make sure all data in table is being written when updating109dist_table = [[0 for j in range(robot_quantity)] for i in range(robot_quantity)]110111# function for solving destination on the loop based on positions of two neighbors112def des_solver(pos_l, pos_r, dist_0, l_d):113# first input is 2D position of neighbor on the left, second for on the right114# dist_0 is the distance between pos_l and pos_r115# l_d is the desired distance to the two neighbors116vect_0 = (pos_l[0]-pos_r[0], pos_l[1]-pos_r[1]) # vector from pos_r to pos_l117midpoint = [(pos_l[0]+pos_r[0])/2, (pos_l[1]+pos_r[1])/2]118if dist_0 >= 2*l_d or dist_0 < 0:119# two neighbors are too far away, destination will be just midpoint120# dist_0 will be passed from dist_table, just in case if -1 is accidentally passed121return midpoint122else:123# get direction perpendicular to the the line of pos_l and pos_r124vect_1 = [-vect_0[1]/dist_0, vect_0[0]/dist_0] # rotate vect_0 ccw for 90 degrees125dist_1 = math.sqrt(l_d*l_d - dist_0*dist_0/4)126# return the point from midpoint, goes along vect_1 for dist_1127return [midpoint[0]+vect_1[0]*dist_1, midpoint[1]+vect_1[1]*dist_1]128129# the loop130sim_exit = False # simulation exit flag131sim_pause = True # simulation pause flag132timer_last = pygame.time.get_ticks() # return number of milliseconds after pygame.init()133timer_now = timer_last # initialize it with timer_last134while not sim_exit:135# exit the program136for event in pygame.event.get():137if event.type == pygame.QUIT:138sim_exit = True # exit with the close window button139if event.type == pygame.KEYUP:140if event.key == pygame.K_SPACE:141sim_pause = not sim_pause # reverse the pause flag142if (event.key == pygame.K_ESCAPE) or (event.key == pygame.K_q):143sim_exit = True # exit with ESC key or Q key144145# skip the rest of the loop if paused146if sim_pause: continue147148# update the physics, control rules and graphics at the same time149timer_now = pygame.time.get_ticks()150if (timer_now - timer_last) > frame_period:151timer_last = timer_now # reset timer152# prepare the distance data for every pair of robots153for i in range(robot_quantity):154for j in range(i+1, robot_quantity):155# status of '-1' does not involve in any connection, so skip156if (robots[i].status == -1) or (robots[j].status == -1):157dist_table[i][j] = -1.0158dist_table[j][i] = -1.0159continue # skip the rest160# it only covers the upper triangle without the diagonal161vect_temp = (robots[i].pos[0]-robots[j].pos[0],162robots[i].pos[1]-robots[j].pos[1])163dist_temp = math.sqrt(vect_temp[0]*vect_temp[0] +164vect_temp[1]*vect_temp[1])165if dist_temp <= comm_range:166# only record distance smaller than communication range167dist_table[i][j] = dist_temp168dist_table[j][i] = dist_temp169else: # ignore the neighbors too far away170dist_table[i][j] = -1.0171dist_table[j][i] = -1.0172# sort the distance in another table, record the index here173index_list = [[] for i in range(robot_quantity)] # index of neighbors in range174# find all robots with non-zero distance175for i in range(robot_quantity):176for j in range(robot_quantity):177if j == i: continue # skip the self, not necessary178if dist_table[i][j] > 0: index_list[i].append(j)179# sort the index_list in the order of increasing distance180for i in range(robot_quantity):181len_temp = len(index_list[i]) # number of neighbors182if (len_temp < 2): continue # no need to sort183else:184# bubble sort185for j in range(len_temp-1):186for k in range(len_temp-1-j):187if (dist_table[i][index_list[i][k]] >188dist_table[i][index_list[i][k+1]]):189# swap the position of the two index190index_temp = index_list[i][k]191index_list[i][k] = index_list[i][k+1]192index_list[i][k+1] = index_temp193# get the status list corresponds to the sorted index_list194status_list = [[] for i in range(robot_quantity)]195for i in range(robot_quantity):196for j in index_list[i]:197status_list[i].append(robots[j].status)198199# instantiate the status transition variables, for the status check process200# the priority is in the following order201s_merging = {} # robot '0' detects robot '2', merging into its group202# key is id of robot '0', value is id of corresponding robot '2'203s_forming2 = {} # two '1_0_0' form the triangle with a '0', becoming '1_0_1'204# key is id of the robot '0', value is the group id of the pair '1_0_0'205# it is designed to be triggered by the new robot '0'206s_forming1 = {} # robot '0' forms the pair with another '0', becoming '1_0_0'207# key is id of robot '0' that discovers other robots '0' in range208# value is a list of robots '0' that are detected, in order of ascending dist209# this variable works the same with "s_init_form" from previous simulations210s_form_done = [] # robot '1_0_1' finishes initial forming, becoming '2'211# list of group id that the triangle forming is done212s_form_checked = [] # auxiliary variable for "s_form_done", indicating checked groups213s_merge_done = [] # robot '1_1' finishes merging, becoming '2'214# list of robots '1' that finished merging215s_group_exp = [] # life time of a group naturally expires, disassemble it216# list of group ids217s_disassemble = [] # disassemble trigger by robots '1' or '2'218# list of lists of group id to be compared for disassembling219s_back_0 = [] # robot '-1' gets back to '0' after life expires220# list of robots '-1'221# All the checkings for robots '1' and '2' getting lost during forming are removed,222# such behaviors should be observable during tests, and program should not run into223# any robot lost once it is free of bugs. Therefore it's not necessary for checking.224225# check 'robots' variable for any status change, schedule for processing in next step226for i in range(robot_quantity):227# for the host robot having status of '0'228if robots[i].status == 0:229# check if this robot has valid neighbors at all230if len(index_list[i]) == 0: continue # skip the neighbor check231# pre-process: classify the robot '1' neighbors into their substatus,232# because robot '0' reacts differently to different substatuses of '1'233# new statuses have been assigned as follows:234# '1_0_0' -> '1.1'235# '1_0_1' -> '1.2'236# '1_1' -> '1.3'237if 1 in status_list[i]:238index_temp = 0239while 1 in status_list[i]:240index_temp = status_list[i].index(1)241robot_temp = index_list[i][index_temp]242status_1_sub = robots[robot_temp].status_1_sub243if status_1_sub == 0:244status_1_0_sub = robots[robot_temp].status_1_0_sub245if status_1_0_sub == 0:246status_list[i][index_temp] = 1.1 # 1.1 for status '1_0_0'247elif status_1_0_sub == 1:248status_list[i][index_temp] = 1.2 # 1.2 for status '1_0_1'249else: # just in case250print("wrong sub status of 1_0")251sys.exit()252elif status_1_sub == 1:253status_list[i][index_temp] = 1.3 # 1.3 for status '1_1'254else: # just in case255prin("wrong sub status of 1")256sys.exit()257# the sequence of processing the neighbors are:258# '2' -> '1.1' -> '0' -> '1.2'&'1.3'259# This means if there is a group of '2', join them; otherwise if there is a260# pair of '1_0_0', join them; otherwise if there is a '0', pair with it; if261# all the above failed, just get bounced away by closest '1_0_1' or '1_1'262# start processing neighbors with status '2'263if 2 in status_list[i]:264# check the group attribution of all the '2'265# get the robot id and group id of the first '2'266index_temp = status_list[i].index(2)267robot_temp = index_list[i][index_temp]268group_temp = robots[robot_temp].group_id269groups_temp = {group_temp: [robot_temp]}270# check if there are still '2' in the list271while 2 in status_list[i][index_temp+1:]:272# indexing the next '2' from index_temp+1273# update the index_temp, robot_temp, group_temp274index_temp = status_list[i].index(2, index_temp+1)275robot_temp = index_list[i][index_temp]276group_temp = robots[robot_temp].group_id277# update groups_temp278if group_temp in groups_temp.keys():279# append this robot in the existing group280groups_temp[group_temp].append(robot_temp)281else:282# add new group id in groups_temp and add this robot283groups_temp[group_temp] = [robot_temp]284# check if there are multiple groups detected from the '2'285target_robot = 0 # the target robot '2' to merge into the group286if len(groups_temp.keys()) == 1:287# there is only one group detected288dist_min = 2*comm_range # smallest distance, start with large one289robot_min = -1 # corresponding robot with min distance290# search the closest '2'291for j in groups_temp.values()[0]:292if dist_table[i][j] < dist_min and dist_table[i][j] > 0:293if robots[j].status_2_avail1:294# at least the interior angle of robot j should be good295it0 = robots[j].key_neighbors[0] # the two neighbors296it1 = robots[j].key_neighbors[1]297if ((robots[it0].status_2_avail1 and robots[j].status_2_avail2[0]) or298(robots[it1].status_2_avail1 and robots[j].status_2_avail2[1])):299# check both avail1 of neighbors and avail2 of robot j300# there should be at least one side available to be merged301dist_min = dist_table[i][j]302robot_min = j303target_robot = robot_min304else:305# there is more than one group detected306# it is designed that no disassembling trigger from robot '0'307# compare which group has the most members in it308member_max = 0 # start with 0 number of members in group309group_max = 0 # corresponding group id with most members310for g_it in groups_temp.keys():311if groups[g_it][0] > member_max:312member_max = groups[g_it][0]313group_max = g_it314# search the closest '2' inside that group315dist_min = 2*comm_range316robot_min = -1317for j in groups_temp[group_max]:318if dist_table[i][j] < dist_min and dist_table[i][j] > 0:319if robots[j].status_2_avail1:320# at least the interior angle of robot j should be good321it0 = robots[j].key_neighbors[0] # the two neighbors322it1 = robots[j].key_neighbors[1]323if ((robots[it0].status_2_avail1 and robots[j].status_2_avail2[0]) or324(robots[it1].status_2_avail1 and robots[j].status_2_avail2[1])):325dist_min = dist_table[i][j]326robot_min = j327target_robot = robot_min328# check if target robot has been located, prepare to merge into its group329if target_robot != -1: # not the initial value of robot_min330# the target robot should have at least one spot availbe to be merged331# status transition scheduled, robot '0' is becoming '1_1'332s_merging[i] = target_robot333# process neighbors with status '1.1'334elif 1.1 in status_list[i]:335# check the group attribution of all '1.1'336# get the robot id and group id of the first 1.1337index_temp = status_list[i].index(1.1)338robot_temp = index_list[i][index_temp]339group_temp = robots[robot_temp].group_id340groups_temp = {group_temp: [robot_temp]}341# check if there are still '1.1' in the list342while 1.1 in status_list[i][index_temp+1:]:343# update the index_temp, robot_temp, group_temp344index_temp = status_list[i].index(1.1, index_temp+1)345robot_temp = index_list[i][index_temp]346group_temp = robots[robot_temp].group_id347# update groups_temp348if group_temp in groups_temp.keys():349# append this robot in the existing group350groups_temp[group_temp].append(robot_temp)351else:352# add new entry of group id in groups_temp and add this robot353groups_temp[group_temp] = [robot_temp]354# check if there are multiple groups detected from the '1.1'355target_robot = 0 # the target robot '1.1' to form triangle with356if len(groups_temp.keys()) == 1:357# there is only one group detected from the '1.1'358dist_min = 2*comm_range # variable for smallest distance359robot_min = -1 # corresponding robot with dist_min360# search the closest '1.1'361for j in groups_temp.values()[0]:362if dist_table[i][j] < dist_min and dist_table[i][j] > 0:363# there is no availability restriction here364dist_min = dist_table[i][j]365robot_min = j366target_robot = robot_min367else:368# there are more than one group detected from the '1.1'369# choose the group that has the most members in it370member_max = 0 # variable for the maximum number of members371group_max = 0 # corresponding group id with member_max372for g_it in groups_temp.keys():373if groups[g_it][0] > member_max:374member_max = groups[g_it][0]375group_max = g_it376# search the closest '1.1' inside that group "group_max"377dist_min = 2*comm_range378robot_min = -1379for j in groups_temp[group_max]:380if dist_table[i][j] < dist_min and dist_table[i][j] > 0:381dist_min = dist_table[i][j]382robot_min = j383target_robot = robot_min384# check if target robot has been located, form the triangle with it385if target_robot != -1: # not the initial value of "robot_min"386group_temp = robots[target_robot].group_id387# status transition scheduled, robot '0' & '1_0_0' are becoming '1_0_1'388s_forming2[i] = group_temp389# it's possible that more than 1 of robot '0' are working with same group390# the first robot '0' that gets processed will win391# process neighbors with status '0'392elif 0 in status_list[i]:393# establish a list of all '0', in order of ascending distance394# this list is to be checked later if pairing is possible and no conflict395# first remove possible '1.2' and '1.3' from status_list[i]396while 1.2 in status_list[i]:397index_temp = status_list[i].index(1.2)398status_list[i].pop(index_temp)399index_list[i].pop(index_temp)400while 1.3 in status_list[i]:401index_temp = status_list[i].index(1.3)402status_list[i].pop(index_temp)403index_list[i].pop(index_temp)404# status transition scheduled, robot '0' is becoming '1_0_0'405s_forming1[i] = index_list[i][:]406# process neighbors with status '1.2' and '1.3'407elif (1.2 in status_list[i]) or (1.3 in status_list[i]):408# find the closest '1.2' or '1.3', and get bounced away by it409dist_min = 2*comm_range410robot_min = -1411# at this time, '1.2' and '1.3' are the only robots left in index_list[i]412for j in index_list[i]:413if dist_table[i][j] < dist_min and dist_table[i][j] > 0:414dist_min = dist_table[i][j]415robot_min = j416# target robot located, the robot_min, should not still be '-1' here417# get bounced away from this robot, update the moving direction418vect_temp = (robots[i].pos[0] - robots[robot_min].pos[0],419robots[i].pos[1] - robots[robot_min].pos[1])420# new orientation is pointing from robot_min to the host robot421robots[i].ori = math.atan2(vect_temp[1], vect_temp[0])422# for the host robot having status of '1'423elif robots[i].status == 1:424# disassemble check, get group attribution of all '1' and '2'425# pop out the '0' from the status_list426while 0 in status_list[i]:427index_temp = status_list[i].index(0)428status_list[i].pop(index_temp)429index_list[i].pop(index_temp)430if len(index_list[i]) > 0: # ensure there is at least one robot around431# start the group attribution dictionaary with first robot432robot_temp = index_list[i][0]433group_temp = robots[robot_temp].group_id434groups_temp = {group_temp:[robot_temp]}435# then check the rest for group attribution436for j in index_list[i][1:]:437group_temp = robots[j].group_id438if group_temp in groups_temp.keys():439groups_temp[group_temp].append(j)440else:441groups_temp[group_temp] = [j]442# check if there are multiple groups detected443if len(groups_temp.keys()) > 1:444# status transition scheduled, to disassemble the minority groups445s_disassemble.append(groups_temp.keys())446# may produce duplicates in s_disassemble, not big problem447# check if any status transition needs to be scheduled448if robots[i].status_1_sub == 0:449if robots[i].status_1_0_sub == 1:450# host robot is in the triangle forming phase451# check if this group has already been scheduled for status transition452g_it = robots[i].group_id453if g_it not in s_form_checked:454# avoid checking same group three times455s_form_checked.append(g_it) # add this group, mark as checked456it0 = robots[i].key_neighbors[0]457it1 = robots[i].key_neighbors[1]458# check all threee sides to see if distances are satisfied459dist_satisfied = True460# if abs(dist_table[i][it0] - loop_space) > space_error:461# dist_satisfied = False462# elif abs(dist_table[i][it1] - loop_space) > space_error:463# dist_satisfied = False464# elif abs(dist_table[it0][it1] - loop_space) > space_error:465# dist_satisfied = False466# release the requirements that as long as three robots can see467# each other, they can form the initial loop of triangle468if dist_table[i][it0] > comm_range: dist_satisfied = False469if dist_table[i][it1] > comm_range: dist_satisfied = False470if dist_table[it0][it1] > comm_range: dist_satisfied = False471if dist_satisfied:472# status transition scheduled, robots '1_0_1' are becoming '2'473s_form_done.append(g_it)474elif robots[i].status_1_sub == 1:475# robot is in the merging phase476it0 = robots[i].key_neighbors[0]477it1 = robots[i].key_neighbors[1]478# check the two distances to see if they are satisfied479dist_satisfied = True480if abs(dist_table[i][it0] - loop_space) > space_error:481dist_satisfied = False482elif abs(dist_table[i][it1] - loop_space) > space_error:483dist_satisfied = False484if dist_satisfied:485# status transition scheduled, robot '1_1' is becoming '2'486s_merge_done.append(i)487# for the host robot having status of '2'488elif robots[i].status == 2:489# disassemble check, get group attribution of all '1' and '2'490# pop out the '0' from the status_list491while 0 in status_list[i]:492index_temp = status_list[i].index(0)493status_list[i].pop(index_temp)494index_list[i].pop(index_temp)495if len(index_list[i]) > 0: # ensure at least one in-group robot around496# start the group attribution dictionaary with first robot497robot_temp = index_list[i][0]498group_temp = robots[robot_temp].group_id499groups_temp = {group_temp:[robot_temp]}500# then check the rest for group attribution501for j in index_list[i][1:]:502group_temp = robots[j].group_id503if group_temp in groups_temp.keys():504groups_temp[group_temp].append(j)505else:506groups_temp[group_temp] = [j]507# check if there are multiple groups detected508if len(groups_temp.keys()) > 1:509# status transition scheduled, to disassemble the minority groups510s_disassemble.append(groups_temp.keys())511# may produce duplicates in s_disassemble, not big problem512# further process the 'status_list[i]' and 'index_list[i]'513# for use in loop adjusting when in the physics update514# the following removes all '1', and keep same group '2' except key neighbors515g_it = robots[i].group_id # group id of host robot516while 1 in status_list[i]:517index_temp = status_list[i].index(1)518status_list[i].pop(index_temp)519index_list[i].pop(index_temp)520index_temp = 0521while 2 in status_list[i][index_temp:]:522index_temp = status_list[i].index(2,index_temp)523robot_temp = index_list[i][index_temp]524if robot_temp in robots[i].key_neighbors:525# remove the same group key neighbors526status_list[i].pop(index_temp)527index_list[i].pop(index_temp)528elif robots[robot_temp].group_id != g_it:529# remove non-same-group robot '2'530status_list[i].pop(index_temp)531index_list[i].pop(index_temp)532else:533# keep the same group '2' that aren't key neighbors534index_temp = index_temp + 1535# for the host robot having status of '-1'536elif robots[i].status == -1:537# check if life time expires538if robots[i].status_n1_life < 0:539# status transition scheduled, robot '-1' is becoming '0'540s_back_0.append(i)541542# check 'groups' variable for any status change543for g_it in groups.keys():544if groups[g_it][5]: continue # already being dominant group545if groups[g_it][0] > robot_quantity/2:546# the group has more than half the totoal number of robots547groups[g_it][5] = True # becoming dominant group548groups[g_it][4] = 100.0 # a large number549print("dominant group established")550if groups[g_it][4] < 0: # life time of a group expires551# schedule operation to disassemble this group552s_group_exp.append(g_it)553554# process the scheduled status change, in the order of the designed priority555# 1.s_merging, robot '0' starts to merge into the group with the robot '2'556for i in s_merging.keys():557j = s_merging[i] # 'j' is the robot that robot 'i' tries to merge with558# the first merge availability of robot 'j' should be true to be here559# discuss the merging availability of robot 'j' on the two sides560g_it = robots[j].group_id561it0 = robots[j].key_neighbors[0]562it1 = robots[j].key_neighbors[1]563# j was available when added to s_merging, but check again in case occupied564# merge availablility on left side565side0_avail = robots[it0].status_2_avail1 and robots[j].status_2_avail2[0]566side0_des = [-1,-1] # destination if merging at left side567if side0_avail:568# calculate the merging destination569side0_des = des_solver(robots[it0].pos, robots[j].pos,570dist_table[it0][j], loop_space)571# merge availablility on right side572side1_avail = robots[it1].status_2_avail1 and robots[j].status_2_avail2[1]573side1_des = [-1,-1]574if side1_avail:575side1_des = des_solver(robots[j].pos, robots[it1].pos,576dist_table[j][it1], loop_space)577# check if there is at least one side available578if side0_avail or side1_avail:579# status transition operations regardless of which side to merge580robots[i].status = 1581robots[i].status_1_sub = 1582robots[i].group_id = g_it583robots[i].key_neighbors = [-1,-1] # initialize it with '-1'584groups[g_it][0] = groups[g_it][0] + 1 # increase group size by 1585groups[g_it][3].append(i)586groups[g_it][4] = groups[g_it][4] + life_incre # increase life time587# deciding which side to merge588side_decision = -1 # 0 for left, 1 for right589if side0_avail and side1_avail:590# both sides are available, calculate distances and compare591vect_temp = (side0_des[0]-robots[i].pos[0], side0_des[1]-robots[i].pos[1])592side0_dist = math.sqrt(vect_temp[0]*vect_temp[0]+593vect_temp[1]*vect_temp[1])594vect_temp = (side1_des[0]-robots[i].pos[0], side1_des[1]-robots[i].pos[1])595side1_dist = math.sqrt(vect_temp[0]*vect_temp[0]+596vect_temp[1]*vect_temp[1])597if side0_dist < side1_dist:598side_decision = 0599else:600side_decision = 1601elif side0_avail and (not side1_avail):602# only left side is available603side_decision = 0604elif side1_avail and (not side0_avail):605# only right side is available606side_decision = 1607# status transition operations according to the side decision608if side_decision == 0: # taking left side609it1 = robots[j].key_neighbors[0]610robots[i].key_neighbors = [it1, j]611robots[i].status_1_1_des = side0_des612vect_temp = (side0_des[0]-robots[i].pos[0], side0_des[1]-robots[i].pos[1])613robots[i].ori = math.atan2(vect_temp[1], vect_temp[0]) # update moving ori614# update for j and it1615robots[j].status_2_avail2[0] = False616robots[it1].status_2_avail2[1] = False617else: # taking right side618it1 = robots[j].key_neighbors[1]619robots[i].key_neighbors = [j, it1]620robots[i].status_1_1_des = side1_des621vect_temp = (side1_des[0]-robots[i].pos[0], side1_des[1]-robots[i].pos[1])622robots[i].ori = math.atan2(vect_temp[1], vect_temp[0]) # update moving ori623# update for j and it1624robots[j].status_2_avail2[1] = False625robots[it1].status_2_avail2[0] = False626# 2.s_forming2, two '1_0_0' forms a triangle with one '0', all becomes '1_0_1'627for i in s_forming2.keys():628g_it = s_forming2[i] # get the group id of the group to form the triangle629if groups[g_it][0] > 2: continue # the group already accepted a robot '0'630# the two group members631it0 = groups[g_it][3][0]632it1 = groups[g_it][3][1]633# deciding robot i is at which side along the vector from it0 to it1634vect_0 = (robots[it1].pos[0]-robots[it0].pos[0],635robots[it1].pos[1]-robots[it0].pos[1]) # vector from it0 to it1636vect_1 = (robots[i].pos[0]-robots[it0].pos[0],637robots[i].pos[1]-robots[it0].pos[1]) # vector from it0 to i638# calculate cross product of vect_0 and vect_1639if (vect_0[0]*vect_1[1]-vect_0[1]*vect_1[0]) > 0:640# i is at left side of vector from it0 to it1641# this is the desired sequence, it goes ccw from it0 to it1 to i642pass643else:644# i is at right side of vector from it0 to it1645# (it's very unlikely that i is on the line of it0 and it1)646# swap value of it0 and it1647# because variable it1 will be treated as robot on i's left, and it0 right648it0 = groups[g_it][3][1]649it1 = groups[g_it][3][0]650# update the robots[i], the new member651robots[i].status = 1652robots[i].status_1_sub = 0653robots[i].status_1_0_sub = 1654robots[i].group_id = g_it655# it1 is on left, it0 is on right, because i is the end index robot on loop656des_new = des_solver(robots[it1].pos, robots[it0].pos,657dist_table[it1][it0], loop_space)658robots[i].status_1_0_1_des = des_new659vect_temp = (des_new[0]-robots[i].pos[0], des_new[1]-robots[i].pos[1])660robots[i].ori = math.atan2(vect_temp[1], vect_temp[0])661robots[i].key_neighbors = [it1, it0]662# update the robots[it0]663robots[it0].status_1_0_sub = 1664des_new = des_solver(robots[i].pos, robots[it1].pos,665dist_table[i][it1], loop_space)666robots[it0].status_1_0_1_des = des_new667vect_temp = (des_new[0]-robots[it0].pos[0], des_new[1]-robots[it0].pos[1])668robots[it0].ori = math.atan2(vect_temp[1], vect_temp[0])669robots[it0].key_neighbors = [i, it1]670# update the robots[it1]671robots[it1].status_1_0_sub = 1672des_new = des_solver(robots[it0].pos, robots[i].pos,673dist_table[it0][i], loop_space)674robots[it1].status_1_0_1_des = des_new675vect_temp = (des_new[0]-robots[it1].pos[0], des_new[1]-robots[it1].pos[1])676robots[it1].ori = math.atan2(vect_temp[1], vect_temp[0])677robots[it1].key_neighbors = [it0, i]678# update the 'groups' variable679groups[g_it][0] = groups[g_it][0] + 1 # increase group size by 1680groups[g_it][3].append(i)681groups[g_it][4] = groups[g_it][4] + life_incre682# 3.s_forming1, robot '0' forms a pair with another '0', both are beoming '1_0_0'683s_pairs = [] # the container for the finalized pairs684while len(s_forming1.keys()) != 0: # there are still robots to be processed685for i in s_forming1.keys():686it = s_forming1[i][0] # index temp687if it in s_forming1.keys():688if s_forming1[it][0] == i: # robot 'it' also recognizes 'i' as closest689s_pairs.append([i, it])690s_forming1.pop(i) # pop out both 'i' and 'it'691s_forming1.pop(it)692break693elif i not in s_forming1[it]:694# usually should not be here unless robots have different sensing range695s_forming1[i].remove(it)696if len(s_forming1[i]) == 0:697s_forming1.pop(i)698break699# have not considered the situation that 'i' in 'it' but not first one700else:701# will be here if robot 'it' is to be bounced away by '1', or merge with '2'702s_forming1[i].remove(it)703if len(s_forming1[i]) == 0:704s_forming1.pop(i)705break706# process the finalized pairs707for pair in s_pairs:708it0 = pair[0] # get indices of the pair709it1 = pair[1]710g_it = random.randint(0, group_id_upper_limit)711while g_it in groups.keys(): # keep generate until not duplicate712g_it = random.randint(0, group_id_upper_limit)713# update the 'robots' variable714robots[it0].status = 1715robots[it1].status = 1716robots[it0].status_1_sub = 0717robots[it1].status_1_sub = 0718robots[it0].status_1_0_sub = 0719robots[it1].status_1_0_sub = 0720robots[it0].group_id = g_it721robots[it1].group_id = g_it722robots[it0].key_neighbors = [it1]723robots[it1].key_neighbors = [it0]724vect_temp = (robots[it1].pos[0]-robots[it0].pos[0],725robots[it1].pos[1]-robots[it0].pos[1]) # from it0 to it1726ori_temp = math.atan2(vect_temp[1], vect_temp[0])727if dist_table[it0][it1] > loop_space:728robots[it0].ori = ori_temp729robots[it1].ori = reset_radian(ori_temp + math.pi)730else:731robots[it0].ori = reset_radian(ori_temp + math.pi)732robots[it1].ori = ori_temp733# update the 'groups' variable734groups[g_it] = [2, 0, [], [it0, it1], 2*life_incre, False] # new entry735# 4.s_form_done, robots '1_0_1' finish triangle forming, all are becoming '2'736for g_it in s_form_done:737# get the three robots738it0 = groups[g_it][3][0]739it1 = groups[g_it][3][1]740it2 = groups[g_it][3][2]741# update the 'robots' variable742robots[it0].status = 2743robots[it1].status = 2744robots[it2].status = 2745robots[it0].vel = 0 # temporarily initialize with 0746robots[it1].vel = 0747robots[it2].vel = 0748robots[it0].status_2_avail2 = [True,True] # both sides of all are available749robots[it1].status_2_avail2 = [True,True]750robots[it2].status_2_avail2 = [True,True]751# update the 'groups' variable752groups[g_it][1] = 3 # three members on the loop now753groups[g_it][2] = [it0, it1, it2] # copy the same robots754groups[g_it][3] = [] # empty the temporary pool755# 5.s_merge_done, robot '1_1' finishes merging, becoming '2'756for i in s_merge_done:757g_it = robots[i].group_id # get the group id758it0 = robots[i].key_neighbors[0]759it1 = robots[i].key_neighbors[1]760# update for robot 'i'761robots[i].status = 2762robots[i].vel = 0763robots[i].status_2_avail2 = [True,True]764# update for robot 'it0' and 'it1'765robots[it0].key_neighbors[1] = i # update right side of left neighbor766robots[it1].key_neighbors[0] = i # update left side of right neighbor767robots[it0].status_2_avail2[1] = True768robots[it1].status_2_avail2[0] = True769# update for 'groups' variable770groups[g_it][1] = groups[g_it][1] + 1 # update number of robots on the loop771groups[g_it][2].append(i) # add to the list of robots on the loop772groups[g_it][3].remove(i) # remove from the list of robots off the loop773# 6.s_group_exp, natural life expiration of the groups774for g_it in s_group_exp:775s_disassemble.append([g_it]) # disassemble together in s_disassemble776# 7.s_disassemble, compare groups and disassemble all but the largest777s_dis_list = [] # list of groups that have been finalized for disassembling778# compare number of members to decide which groups to disassemble779for gs_it in s_disassemble:780if len(gs_it) == 1: # disassemble trigger from other sources781if gs_it[0] not in s_dis_list:782s_dis_list.append(gs_it[0])783else:784# compare which group has the most members, and disassemble the rest785g_temp = gs_it[:]786member_max = 0 # variable for maximum number of members787group_max = -1 # corresponding group id with member_max788for g_it in g_temp:789if groups[g_it][0] > member_max:790member_max = groups[g_it][0]791group_max = g_it792g_temp.remove(group_max) # remove the group with the most members793for g_it in g_temp:794if g_it not in s_dis_list:795s_dis_list.append(g_it)796# start disassembling797for g_it in s_dis_list:798# update the 'robots' variable799for i in groups[g_it][3]: # for robots off the line800robots[i].vel = const_vel # restore the faster speed801robots[i].status = -1802robots[i].status_n1_life = random.randint(n1_life_lower, n1_life_upper)803robots[i].ori = random.random() * 2*math.pi - math.pi804# continue the idea of exploding robots, but it's much more straightforward here805for i in groups[g_it][2]: # for robots on the loop806robots[i].vel = const_vel807robots[i].status = -1808robots[i].status_n1_life = random.randint(n1_life_lower, n1_life_upper)809it0 = robots[i].key_neighbors[0]810it1 = robots[i].key_neighbors[1]811vect_0 = (robots[it0].pos[0]-robots[it1].pos[0],812robots[it0].pos[1]-robots[it1].pos[1]) # from it1 to it0813vect_1 = (-vect_0[1], vect_0[0]) # rotate vect_0 90 degrees ccw814robots[i].ori = math.atan2(vect_1[1], vect_1[0]) # exploding orientation815# update 'groups' variable816groups.pop(g_it)817# 8.s_back_0, life time of robot '-1' expires, becoming '0'818for i in s_back_0:819# still maintaining the old moving direction and velocity820robots[i].status = 0821822# update the physics(pos, vel, and ori), and wall bouncing, life decrease of '-1'823for i in range(robot_quantity):824# check if out of boundaries, same algorithm from previous line formation program825# change only direction of velocity826if robots[i].pos[0] >= world_size[0]: # out of right boundary827if math.cos(robots[i].ori) > 0: # velocity on x is pointing right828robots[i].ori = reset_radian(2*(math.pi/2) - robots[i].ori)829elif robots[i].pos[0] <= 0: # out of left boundary830if math.cos(robots[i].ori) < 0: # velocity on x is pointing left831robots[i].ori = reset_radian(2*(math.pi/2) - robots[i].ori)832if robots[i].pos[1] >= world_size[1]: # out of top boundary833if math.sin(robots[i].ori) > 0: # velocity on y is pointing up834robots[i].ori = reset_radian(2*(0) - robots[i].ori)835elif robots[i].pos[1] <= 0: # out of bottom boundary836if math.sin(robots[i].ori) < 0: # velocity on y is pointing down837robots[i].ori = reset_radian(2*(0) - robots[i].ori)838# update one step of distance839travel_dist = robots[i].vel * frame_period/1000.0840robots[i].pos[0] = robots[i].pos[0] + travel_dist*math.cos(robots[i].ori)841robots[i].pos[1] = robots[i].pos[1] + travel_dist*math.sin(robots[i].ori)842# update moving direction and destination of robot '1'843if robots[i].status == 1:844if robots[i].status_1_sub == 0 and robots[i].status_1_0_sub == 0:845# for the pair forming robots '1_0_0'846it0 = robots[i].key_neighbors[0]847if abs(dist_table[i][it0] - loop_space) < space_error:848# stop the robot if distance is good849robots[i].vel = 0850else:851vect_temp = (robots[it0].pos[0]-robots[i].pos[0],852robots[it0].pos[1]-robots[i].pos[1]) # from i to it0853ori_temp = math.atan2(vect_temp[1], vect_temp[0])854# update the moving direction855if dist_table[i][it0] > loop_space:856robots[i].ori = ori_temp857else:858robots[i].ori = reset_radian(ori_temp + math.pi)859elif ((robots[i].status_1_sub == 0 and robots[i].status_1_0_sub == 1) or860robots[i].status_1_sub == 1):861# for the triangle forming robots '1_0_1', or merging robots '1_1'862it0 = robots[i].key_neighbors[0]863it1 = robots[i].key_neighbors[1]864new_des = des_solver(robots[it0].pos, robots[it1].pos,865dist_table[it0][it1], loop_space)866# update the new destination867if robots[i].status_1_sub == 0: # for '1_0_1'868robots[i].status_1_0_1_des = new_des869else: # for '1_1'870robots[i].status_1_1_des = new_des871vect_temp = (new_des[0]-robots[i].pos[0],872new_des[1]-robots[i].pos[1])873robots[i].ori = math.atan2(vect_temp[1], vect_temp[0])874# update moving direction, velocity and first merge availability of robot '2'875elif robots[i].status == 2:876it0 = robots[i].key_neighbors[0]877it1 = robots[i].key_neighbors[1]878vect_0 = (robots[it0].pos[0]-robots[i].pos[0],879robots[it0].pos[1]-robots[i].pos[1]) # from i to it0880vect_1 = (robots[it1].pos[0]-robots[i].pos[0],881robots[it1].pos[1]-robots[i].pos[1]) # from i to it1882ori_0 = math.atan2(vect_0[1], vect_0[0])883ori_1 = math.atan2(vect_1[1], vect_1[0])884# calculate the interior angle of the polygon at robot 'i'885angle_inter = ori_0 - ori_1 # angle from vect_1 to vect_0886# reset to pisitive angle, do not use reset_radian() here887if angle_inter < 0: angle_inter = angle_inter + 2*math.pi888# update the first merge availability889if angle_inter > math.pi:890# only set to false if exceed the upper limit, not the lower limit891robots[i].status_2_avail1 = False892else:893robots[i].status_2_avail1 = True894# update moving direction and velocity895# also keep away from same group robots '2' that are not key neighbors896new_des = des_solver(robots[it0].pos, robots[it1].pos,897dist_table[it0][it1], loop_space)898# update moving orientation and velocity based on calculated new destination899vect_temp = (new_des[0]-robots[i].pos[0],900new_des[1]-robots[i].pos[1])901robots[i].ori = math.atan2(vect_temp[1], vect_temp[0])902dist_temp = math.sqrt(vect_temp[0]*vect_temp[0]+903vect_temp[1]*vect_temp[1])904robots[i].vel = dist_temp * adjust_vel_coef905if len(index_list[i]) > 0:906# there are same group non-neighbor robots '2' around907# find the closest one within them, and move away from it908dist_min = 2*comm_range909robot_min = -1910for j in index_list[i]:911if dist_table[i][j] < dist_min and dist_table[i][j] > 0:912dist_min = dist_table[i][j]913robot_min = j914# robot_min is the acquired closest robot915vect_temp = (robots[i].pos[0]-robots[robot_min].pos[0],916robots[i].pos[1]-robots[robot_min].pos[1])917robots[i].ori = math.atan2(vect_temp[1], vect_temp[0])918# not updating velocity here, use the same urgency of moving to ideal des919# decrease life time of robot '-1'920elif robots[i].status == -1:921robots[i].status_n1_life = robots[i].status_n1_life - frame_period/1000.0922# life time decrease of the groups923for g_it in groups.keys():924if groups[g_it][5]: continue # skip the dominant group925groups[g_it][4] = groups[g_it][4] - frame_period/1000.0926927# graphics update928screen.fill(background_color)929# draw the robots930for i in range(robot_quantity):931display_pos = world_to_display(robots[i].pos, world_size, screen_size)932# get color of the robot933color_temp = ()934if robots[i].status == 0:935color_temp = robot_0_color936elif robots[i].status == 1:937if robots[i].status_1_sub == 0:938if robots[i].status_1_0_sub == 0:939color_temp = robot_1_0_0_color940elif robots[i].status_1_0_sub == 1:941color_temp = robot_1_0_1_color942elif robots[i].status_1_sub == 1:943color_temp = robot_1_1_color944elif robots[i].status == 2:945color_temp = robot_2_color946elif robots[i].status == -1:947color_temp = robot_n1_color948# draw the robot as a small solid circle949pygame.draw.circle(screen, color_temp, display_pos, robot_size, 0) # fill the circle950# draw the line segments for the loops951for g_it in groups.keys():952if groups[g_it][1] > 0:953# this group is not in the initial forming phase954# will not use the groups[g_it][2] to draw the line, it's not ordered955# start with first robot in groups[g_it][2], and use key neighbors to continue956# this also acts as a way to check if key_neighbors are in good shape957it_start = groups[g_it][2][0]958# do one iteration first959it0 = it_start960it1 = robots[it0].key_neighbors[1] # going ccw961it0_disp = world_to_display(robots[it0].pos, world_size, screen_size)962it1_disp = world_to_display(robots[it1].pos, world_size, screen_size)963pygame.draw.line(screen, robot_2_color, it0_disp, it1_disp)964line_count = 1 # count number of line segments that have been drawn965while it1 != it_start: # keep iterating966it0 = it1967it1 = robots[it0].key_neighbors[1]968it0_disp = world_to_display(robots[it0].pos, world_size, screen_size)969it1_disp = world_to_display(robots[it1].pos, world_size, screen_size)970pygame.draw.line(screen, robot_2_color, it0_disp, it1_disp)971line_count = line_count + 1972# check if number of drawed line segments is correct973if line_count != groups[g_it][1]:974print "group {} has {} nodes but {} line segments".format(g_it, groups[g_it][1],975line_count)976pygame.display.update()977978pygame.quit()979980981982983