Path: blob/master/trigridnet_role_assignment.py
104 views
# This simulation tests a distributed role assignment algorithm, for robot swarm starting1# at arbitrary triangle grid network, to perform one-to-one role assignment. The assigned2# roles are abstract and represented as index numbers. The primary goal of this role3# assignment algorithm is to have all nodes agree on the one-to-one assignment scheme,4# instead of coming up with the best assignment scheme.56# input arguments:7# '-f': filename of the triangle grid network89# Inter-node communication is used to let one node know the status of another node that10# is not directly connected. Enabling message relay is what I consider the most convenient11# way to resolve conflict when two nodes decide on the same assigned role. Although in this12# way, it is possible for each node to have the information of all the other nodes, and13# thus it could become a distributed master control algorithm, we still consider the proposed14# algorithm a distributed one. Because we limit the information each node will store, and15# the local computation performed, so the algorithm is still robust.1617# The communication delay is also reflected in the simulation, one step in simulation will18# update one step of message transmission. Color is used to highlight the conflicts in the19# role assignment, instead of showing convergence of decisions.2021# visualization design:22# solid black circle for undetermined role assignment scheme23# solid colored circle for conflicts in role assignment24# extra circle on node for locally converged role assignment scheme252627import pygame28import matplotlib.pyplot as plt29from trigridnet_generator import *30from formation_functions import *31import numpy as np32import os, getopt, sys, time, random3334import pandas as pd35pd.set_option('display.max_columns', None)36# print pd.DataFrame(gradients, range(net_size), range(net_size))3738net_folder = 'trigrid-networks'39net_filename = '30-1' # default network40net_size = 30 # default network size41net_filepath = os.path.join(os.getcwd(), net_folder, net_filename)4243# read command line options44try:45opts, args = getopt.getopt(sys.argv[1:], 'f:')46except getopt.GetoptError as err:47print str(err)48sys.exit()49for opt,arg in opts:50if opt == '-f':51net_filename = arg52net_filepath = os.path.join(os.getcwd(), net_folder, net_filename)53# check if this file exists54if not os.path.isfile(net_filepath):55print "{} does not exist".format(net_filename)56sys.exit()57# parse the network size58net_size = int(net_filename.split('-')[0])5960# read the network from file61nodes_tri = []62# nodes_tri: node positions in the triangle grid network63# nodes_cart: node positions in Cartesian coordinates64# nodes_disp: node positions for display65f = open(net_filepath, 'r')66new_line = f.readline()67while len(new_line) != 0:68pos_str = new_line[0:-1].split(' ')69pos = [int(pos_str[0]), int(pos_str[1])]70nodes_tri.append(pos)71new_line = f.readline()7273# generate the connection matrix, 0 for not connected, 1 for connected74connections = np.zeros((net_size, net_size))75for i in range(net_size):76for j in range(i+1, net_size):77diff_x = nodes_tri[i][0] - nodes_tri[j][0]78diff_y = nodes_tri[i][1] - nodes_tri[j][1]79if abs(diff_x) + abs(diff_y) == 1 or diff_x * diff_y == -1:80connections[i,j] = 181connections[j,i] = 182# connection list indexed by node83connection_lists = []84for i in range(net_size):85connection_lists.append(list(np.where(connections[i] == 1)[0]))8687# plot the network as dots and lines in pygame window88pygame.init()89font = pygame.font.SysFont("Cabin", 14)90nodes_cart = np.array([trigrid_to_cartesian(pos) for pos in nodes_tri])91# find appropriate window size to fit current network92(xmin, ymin) = np.amin(nodes_cart, axis=0)93(xmax, ymax) = np.amax(nodes_cart, axis=0)94clearance = 2.095world_size = (xmax-xmin + clearance, ymax-ymin + clearance)96pixels_per_length = 50 # corresponds to 1.0 length in cartesian world97screen_size = (int(round(world_size[0] * pixels_per_length)),98int(round(world_size[1] * pixels_per_length)))99node_size = 8100line_width = 4101converge_ring_size = 12102# colors103color_white = (255,255,255)104color_black = (0,0,0)105distinct_color_set = ((230,25,75), (60,180,75), (255,225,25), (0,130,200), (245,130,48),106(145,30,180), (70,240,240), (240,50,230), (210,245,60), (250,190,190),107(0,128,128), (230,190,255), (170,110,40), (255,250,200), (128,0,0),108(170,255,195), (128,128,0), (255,215,180), (0,0,128), (128,128,128))109color_quantity = len(distinct_color_set)110# simulation window111icon = pygame.image.load("icon_geometry_art.jpg")112pygame.display.set_icon(icon)113screen = pygame.display.set_mode(screen_size)114pygame.display.set_caption("Role Assignment on 2D Triangle Grid Network")115116# node display positions117center_temp = (nodes_cart.max(axis=0) + nodes_cart.min(axis=0))/2.0118nodes_cart = nodes_cart - center_temp + (world_size[0]/2.0, world_size[1]/2.0)119nodes_disp = [world_to_display(nodes_cart[i], world_size, screen_size)120for i in range(net_size)]121122# draw the network for the first time123screen.fill(color_white)124for i in range(net_size):125for j in range(i+1, net_size):126if connections[i,j]:127pygame.draw.line(screen, color_black, nodes_disp[i], nodes_disp[j], line_width)128for i in range(net_size):129pygame.draw.circle(screen, color_black, nodes_disp[i], node_size, 0)130# text = font.render(str(i), True, color_black)131# screen.blit(text, (nodes_disp[i][0]+12, nodes_disp[i][1]-12))132pygame.display.update()133134raw_input("press <ENTER> to continue")135136########## the role assignment algorithm ##########137138# Gradient value method for information flow control:139# This gradient method was first used in the early simulations of Kilobot project, for a robot140# to localize itself in a swarm in order to do formation control. The gradient value indicates141# the distance from a particular source, and can be used here to guarantee the information142# flows only forward, instead of backward. If the source node has gradient value of 0, all the143# nodes next to it will have gradient value of 1, then nodes next to them have gradient value144# of 2. These nodes are forming nested-ring patterns. The message will only be transmitted from145# a low gradient node to a high gradient one. In this way, one message from source will travel146# through all other nodes, without resonating infinitely inside the network. Since every node147# in this application will transmit its own message, the node needs to calculate the gradient148# value of all message sources.149150# To construct the gradient values in a distributed way, when messages are received from a new151# message source, the node will take the minimum gradient value plus 1 as its gradient for152# that message source. In this way each node will build the gradient values on-the-go for any153# other message source. A little more complicated algorithm for constructing gradient values154# is also developed to deal with any unstable communication for message transmissions.155156# However, to simplify the role assignment simulation, the gradient map is pre-calculated.157# Although I could use algorithm similar in the holistic dependency calculation, a new one that158# searching the shortest path between any two nodes is investigated in the following.159gradients = np.copy(connections) # build gradient map on the connection map160# gradients[i,j] indicates gradient value of node j, to message source i161pool_gradient = 1 # gradients of the connections in the pool162pool_conn = {}163for i in range(net_size):164pool_conn[i] = connection_lists[i][:] # start with gradient 1 connections165while len(pool_conn.keys()) != 0:166source_deactivate = []167for source in pool_conn:168targets_temp = [] # the new targets169for target in pool_conn[source]:170for target_new in connection_lists[target]:171if target_new == source: continue # skip itself172if gradients[source, target_new] == 0:173gradients[source, target_new] = pool_gradient + 1174targets_temp.append(target_new)175if len(targets_temp) == 0:176source_deactivate.append(source)177else:178pool_conn[source] = targets_temp[:] # update with new targets179for source in source_deactivate:180pool_conn.pop(source) # remove the finished sources181pool_gradient = pool_gradient + 1182183# calculate the relative gradient values184gradients_rel = []185# gradients_rel[i][j,k] refers to gradient of k relative to j with message source i186for i in range(net_size): # message source i187gradient_temp = np.zeros((net_size, net_size))188for j in range(net_size): # in the view point of j189gradient_temp[j] = gradients[i] - gradients[i,j]190gradients_rel.append(gradient_temp)191192# list the neighbors a node can send message to regarding a message source193neighbors_send = [[[] for j in range(net_size)] for i in range(net_size)]194# neighbors_send[i][j][k] means, if message from source i is received in j,195# it should be send to k196for i in range(net_size): # message source i197for j in range(net_size): # in the view point of j198for neighbor in connection_lists[j]:199if gradients_rel[i][j,neighbor] == 1:200neighbors_send[i][j].append(neighbor)201202# generate the initial preference distribution203pref_dist = np.random.rand(net_size, net_size) # no need to normalize it204initial_roles = np.argmax(pref_dist, axis=1) # the chosen role205206# the local assignment information207local_role_assignment = [[[-1, 0, -1] for j in range(net_size)] for i in range(net_size)]208# local_role_assignment[i][j] is local assignment information of node i for node j209# first number is chosen role, second is probability, third is time stamp210local_node_assignment = [[[] for j in range(net_size)] for i in range(net_size)]211# local_node_assignment[i][j] is local assignment of node i for role j212# contains a list of nodes that choose role j213# populate the chosen role of itself to the local assignment information214for i in range(net_size):215local_role_assignment[i][i][0] = initial_roles[i]216local_role_assignment[i][i][1] = pref_dist[i, initial_roles[i]]217local_role_assignment[i][i][2] = 0218local_node_assignment[i][initial_roles[i]].append(i)219220# received message container for all nodes221message_rx = [[] for i in range(net_size)]222# for each message entry, it containts:223# message[0]: ID of message source224# message[1]: its preferred role225# message[2]: probability of chosen role226# message[3]: time stamp227# all nodes transmit once their chosen role before the loop228transmission_total = 0 # count message transmissions for each iteration229iter_count = 0 # also used as time stamp in message230for source in range(net_size):231chosen_role = local_role_assignment[source][source][0]232message_temp = [source, chosen_role, pref_dist[source, chosen_role], iter_count]233for target in connection_lists[source]: # send to all neighbors234message_rx[target].append(message_temp)235transmission_total = transmission_total + 1236role_color = [0 for i in range(net_size)] # colors for a conflicting role237# Dynamically manage color for conflicting nodes is unnecessarily complicated, might as238# well assign the colors in advance.239role_index_pool = range(net_size)240random.shuffle(role_index_pool)241color_index_pool = range(color_quantity)242random.shuffle(color_index_pool)243while len(role_index_pool) != 0:244role_color[role_index_pool[0]] = color_index_pool[0]245role_index_pool.pop(0)246color_index_pool.pop(0)247if len(color_index_pool) == 0:248color_index_pool = range(color_quantity)249random.shuffle(color_index_pool)250251# flags252transmit_flag = [[False for j in range(net_size)] for i in range(net_size)]253# whether node i should transmit received message of node j254change_flag = [False for i in range(net_size)]255# whether node i should change its chosen role256scheme_converged = [False for i in range(net_size)]257258sim_exit = False259sim_pause = False260time_now = pygame.time.get_ticks()261time_last = time_now262time_period = 2000263speed_control = True # set False to skip speed control264flash_delay = 200265while not sim_exit:266# exit the program by close window button, or Esc or Q on keyboard267for event in pygame.event.get():268if event.type == pygame.QUIT:269sim_exit = True # exit with the close window button270if event.type == pygame.KEYUP:271if event.key == pygame.K_SPACE:272sim_pause = not sim_pause # reverse the pause flag273if (event.key == pygame.K_ESCAPE) or (event.key == pygame.K_q):274sim_exit = True # exit with ESC key or Q key275276# skip the rest if paused277if sim_pause: continue278279# simulation speed control280time_now = pygame.time.get_ticks()281if (time_now - time_last) > time_period:282time_last = time_now283else:284continue285286iter_count = iter_count + 1287288# process the received messages289# transfer messages to the processing buffer, then empty the message receiver290message_rx_buf = [[[k for k in j] for j in i] for i in message_rx]291message_rx = [[] for i in range(net_size)]292yield_nodes = [] # the nodes that are yielding on chosen roles293yield_roles = [] # the old roles of yield_nodes before yielding294for i in range(net_size): # messages received by node i295for message in message_rx_buf[i]:296source = message[0]297role = message[1]298probability = message[2]299time_stamp = message[3]300if source == i:301print "error, node {} receives message of itself".format(i)302sys.exit()303if time_stamp > local_role_assignment[i][source][2]:304# received message will only take any effect if time stamp is new305# update local_node_assignment306role_old = local_role_assignment[i][source][0]307if role_old >= 0: # has been initialized before, not -1308local_node_assignment[i][role_old].remove(source)309local_node_assignment[i][role].append(source)310# update local_role_assignment311local_role_assignment[i][source][0] = role312local_role_assignment[i][source][1] = probability313local_role_assignment[i][source][2] = time_stamp314transmit_flag[i][source] = True315# check conflict with itself316if role == local_role_assignment[i][i][0]:317if probability >= pref_dist[i, local_role_assignment[i][i][0]]:318# change its choice after all message received319change_flag[i] = True320yield_nodes.append(i)321yield_roles.append(local_role_assignment[i][i][0])322# change the choice of role for those decide to323for i in range(net_size):324if change_flag[i]:325change_flag[i] = False326role_old = local_role_assignment[i][i][0]327pref_dist_temp = np.copy(pref_dist[i])328pref_dist_temp[local_role_assignment[i][i][0]] = -1329# set to negative to avoid being chosen330for j in range(net_size):331if len(local_node_assignment[i][j]) != 0:332# eliminate those choices that have been taken333pref_dist_temp[j] = -1334role_new = np.argmax(pref_dist_temp)335if pref_dist_temp[role_new] < 0:336print "error, node {} has no available role".format(i)337sys.exit()338# role_new is good to go339# update local_node_assignment340local_node_assignment[i][role_old].remove(i)341local_node_assignment[i][role_new].append(i)342# update local_role_assignment343local_role_assignment[i][i][0] = role_new344local_role_assignment[i][i][1] = pref_dist[i][role_new]345local_role_assignment[i][i][2] = iter_count346transmit_flag[i][i] = True347# transmit the received messages or initial new message transmission348transmission_total = 0349for transmitter in range(net_size): # transmitter node350for source in range(net_size): # message is for this source node351if transmit_flag[transmitter][source]:352transmit_flag[transmitter][source] = False353message_temp = [source, local_role_assignment[transmitter][source][0],354local_role_assignment[transmitter][source][1],355local_role_assignment[transmitter][source][2]]356for target in neighbors_send[source][transmitter]:357message_rx[target].append(message_temp)358transmission_total = transmission_total + 1359360# check if role assignment scheme is converged at individual node361for i in range(net_size):362if not scheme_converged[i]:363converged = True364for j in range(net_size):365if len(local_node_assignment[i][j]) != 1:366converged = False367break368if converged:369scheme_converged[i] = True370371# for display, scan the nodes that have detected conflict but not yielding372persist_nodes = []373for i in range(net_size):374if i in yield_nodes: continue375if len(local_node_assignment[i][local_role_assignment[i][i][0]]) > 1:376persist_nodes.append(i)377378# debug print379print "iteration {}, total transmission {}".format(iter_count, transmission_total)380381# update the display382for i in range(net_size):383for j in range(i+1, net_size):384if connections[i,j]:385pygame.draw.line(screen, color_black, nodes_disp[i], nodes_disp[j], line_width)386for i in range(net_size):387pygame.draw.circle(screen, color_black, nodes_disp[i], node_size, 0)388# draw the persisting nodes with color of conflicting role389for i in persist_nodes:390pygame.draw.circle(screen, distinct_color_set[role_color[local_role_assignment[i][i][0]]],391nodes_disp[i], node_size, 0)392# draw extra ring on node if local scheme has converged393for i in range(net_size):394if scheme_converged[i]:395pygame.draw.circle(screen, color_black, nodes_disp[i], converge_ring_size, 2)396pygame.display.update()397# flash the yielding nodes with color of old role398for _ in range(3):399# change to color400for i in range(len(yield_nodes)):401pygame.draw.circle(screen, distinct_color_set[role_color[yield_roles[i]]],402nodes_disp[yield_nodes[i]], node_size, 0)403pygame.display.update()404pygame.time.delay(flash_delay)405# change to black406for i in range(len(yield_nodes)):407pygame.draw.circle(screen, color_black,408nodes_disp[yield_nodes[i]], node_size, 0)409pygame.display.update()410pygame.time.delay(flash_delay)411412# exit the simulation if all role assignment schemes have converged413all_converged = scheme_converged[0]414for i in range(1, net_size):415all_converged = all_converged and scheme_converged[i]416if not all_converged: break417if all_converged: sim_exit = True418419# hold the simulation window to exit manually420raw_input("role assignment finished, press <ENTER> to exit")421422423424425426