diff --git a/showml/antnet/ant.py b/showml/antnet/ant.py new file mode 100644 index 0000000..6ab57ff --- /dev/null +++ b/showml/antnet/ant.py @@ -0,0 +1,111 @@ +import random +from graph import Graph + +class Ant(): + graph = None + + def __init__(self, source, destination): + self.source = source + self.destination = destination + self.current_node = self.source + self.path_taken = [] + + # def update_graph(self, graph): + # self.graph = graph + + def update_destination(self, destination): + self.destination = destination + + def update_source(self, source): + self.source = source + + def update_current(self, curr): + self.current_node = curr + + def reset_paths(self): + self.path_taken = [] + self.path_to_take = [] + + def take_step(self): + # Return true if already reached destination + if self.current_node == self.destination: + return True + + Ant.graph.get_node(self.current_node)['visited'] = True + + # Take a random neighbor from current position + all_neighbors = Ant.graph.get_neighbors(self.current_node) + phero_values = Ant.graph.get_pheromones(self.current_node) + travel_length = Ant.graph.get_travel_times(self.current_node) + alpha = Ant.graph.get_alpha() + beta = Ant.graph.get_beta() + + # If there are no neighbors. Special case - isolated intersection + if len(all_neighbors) == 0: + return False + + # Already visited or not + chosen_neighbors = [] + for neigh in all_neighbors: + if not Ant.graph.get_node(neigh)['visited']: + chosen_neighbors.append(neigh) + + # Randomly select a neighbor from current node and add that in the + # path_taken list + total = 0.0 + probabilities = {} + for i in range(len(all_neighbors)): + total += (phero_values[i]**alpha)*((1/travel_length[i])**beta) + + # print(chosen_neighbors) + for i in range(len(chosen_neighbors)): + probabilities[chosen_neighbors[i]] = ((phero_values[i]**alpha)*((1/travel_length[i])**beta))/total + + sorted_probabilities = {k: v for k, v in sorted(probabilities.items(), key=lambda item: -item[1])} + sorted_neighbors = list(sorted_probabilities) + sorted_values = list(sorted_probabilities.values()) + + # Selection using a threshold value epsilon + eps = 0.1 + candidates = [] + + if len(sorted_probabilities) == 1: + candidates.append(sorted_neighbors[0]) + else: + for i in range(len(sorted_probabilities)-1): + if (sorted_values[i] - sorted_values[i+1]) < eps: + candidates.append(sorted_neighbors[i]) + if i == len(sorted_probabilities) - 2: + candidates.append(sorted_neighbors[i+1]) + else: + candidates.append(sorted_neighbors[i]) + break + + # Selection using random weighted choice with the probabilities + selected_neighbor = random.choices(sorted_neighbors, weights = sorted_values, k=1)[0] + + Ant.graph.update_pheromones(self.current_node , selected_neighbor) + + self.path_taken.append({ + 'start': self.current_node, + 'end': selected_neighbor, + 'time_spent': Ant.graph.get_edge_time(self.current_node, selected_neighbor) + }) + + # Update current position + self.current_node = selected_neighbor + + return False + + def get_path_taken(self): + return self.path_taken + + def get_graph(self): + return Ant.graph + + def get_time_spent(self): + time_spent = 0 + for path in self.path_taken: + time_spent += path['time_spent'] + + return time_spent \ No newline at end of file diff --git a/showml/antnet/antnet.py b/showml/antnet/antnet.py new file mode 100644 index 0000000..97d3bdb --- /dev/null +++ b/showml/antnet/antnet.py @@ -0,0 +1,170 @@ +from ant import Ant +import random + +MIN_VAL = -99999 + +class AntNet(Ant): + graph = None + def __init__(self, source, destination, alpha): + super().__init__(source, destination) + self.alpha = alpha + + def get_L(self, node, neighbor): + cost = node['neighbors'][neighbor] + sum_cost = 0 + for n in node['neighbors']: + sum_cost += node['neighbors'][n] + + return 1 - (cost/sum_cost) + + def get_neighbor_prob(self, node): + node = AntNet.graph.get_node(node) + + chosen_neighbors = [] + all_neighbors = [] + for n in node['neighbors']: + all_neighbors.append(n) + if not AntNet.graph.get_node(n)['visited']: + chosen_neighbors.append(n) + + probs = [] + if len(chosen_neighbors) > 0: + for n in chosen_neighbors: + L = self.get_L(node, n) + prob = (node['routing_table'][n] + self.alpha * self.get_L(node, n)) / (1 + self.alpha * (len(node['neighbors']) - 1)) + probs.append((n, prob)) + return probs + + next_neighbor = random.choice(all_neighbors) + return [(next_neighbor, 1)] + + + def remove_cycle_if_any(self): + if len(self.path_taken) <= 1: + return + + cycle_detected = False + start_idx = 0 + end_idx = 1 + for i in range(len(self.path_taken)): + start_idx = i + for j in range(i+1, len(self.path_taken)): + end_idx = j + start_path = self.path_taken[i] + end_path = self.path_taken[j] + + if end_path['end'] == start_path['start']: + cycle_detected = True + break + if cycle_detected: + break + + if cycle_detected: + # print("CYCLE -> ", self.path_taken) + del self.path_taken[start_idx+1:] + # print("CYCLE REMOVED -> ", self.path_taken) + self.current_node = self.path_taken[start_idx]['end'] + + def take_forward_step(self): + self.remove_cycle_if_any() + node = self.current_node + + AntNet.graph.get_node(node)['visited'] = True + if node == self.destination: + return node + + probs = self.get_neighbor_prob(node) + + next_neighbor = None + max_prob = MIN_VAL + for prob in probs: + if not AntNet.graph.get_node(prob[0])['visited']: + next_neighbor = prob[0] + break + if prob[1] > max_prob: + max_prob = prob[1] + next_neighbor = prob[0] + + if next_neighbor is not None: + self.path_taken.append({ + 'start': self.current_node, + 'end': next_neighbor, + 'time_taken': AntNet.graph.get_edge_time(self.current_node, next_neighbor) + }) + + self.current_node = next_neighbor + return self.current_node + + def take_lazy_step(self): + if self.current_node == self.destination: + return self.current_node + + routing_table = AntNet.graph.get_node(self.current_node)['routing_table'] + # print(routing_table) + # print(max(routing_table, key=routing_table.get)) + self.current_node = max(routing_table, key=routing_table.get) + return self.current_node + + + # Reverse the path taken + def reverse_path(self): + self.path_to_take = self.path_taken.copy() + self.path_to_take.reverse() + for path in self.path_to_take: + path['start'], path['end'] = path['end'], path['start'] + + def go_backward(self): + total_time_taken = 0 + # if len(self.path_to_take) == 0: + # print(self.source, self.destination) + # print("path to take", self.path_to_take) + for node in self.path_to_take: + total_time_taken += node['time_taken'] + # for backward ant, the actual destination is now the source since we swapped it earlier + AntNet.graph.update_traffic_stat(node['end'], self.source, node['start'], total_time_taken) + +def runAntNet(G, true_source, destination, c1, c2, lm): + G.set_antnet_hyperparams(c1, c2, lm) + iterations = 500 + sources = G.get_all_nodes() + sources = [s for s in sources if s != destination] + ants = [] + for s in sources: + ants.append(AntNet(s, destination, 0.2)) + + for _ in range(1000): + AntNet.graph = G + for ant in ants: + ant.reset_paths() + ant.update_destination(destination) + ant.update_source(random.choice(sources)) + ant.update_current(ant.source) + + for ant in ants: + for i in range(iterations): + node = ant.take_forward_step() + if node == ant.destination: + break + + # Skip ants that did not reach destination + if node != ant.destination: + print("Ant did not reach destination!\n") + continue + + # Backward ant + ant.reverse_path() + ant.update_destination(ant.source) + ant.update_source(destination) + + ant.go_backward() + + lazy_ant = AntNet(true_source, destination, 0) + path_taken = [] + for i in range(iterations): + path_taken.append(lazy_ant.current_node) + node = lazy_ant.take_lazy_step() + if node == destination: + path_taken.append(node) + break + + return path_taken \ No newline at end of file diff --git a/showml/antnet/graph.py b/showml/antnet/graph.py new file mode 100644 index 0000000..86150c8 --- /dev/null +++ b/showml/antnet/graph.py @@ -0,0 +1,268 @@ +import random +from pprint import pprint +import math + +class Graph: + + def __init__(self, alpha=0.9, beta=0.1, evap = 0.1): + """ + graph:- + { + 'A':{ + 'visited': False, + 'neighbors':{ + 'B': 3, + 'C': 9 + }, + 'pheros': { + 'B': 5 + }, + 'routing_table': { + 'B': 0.2, + 'C': 0.1 + }, + 'traffic_stat': { + 'B': { + 'mean': 0.4, + 'std': 0.2, + 'W': [3, 4, 5] + }, + 'C': { + 'mean': 0.4, + 'std': 0.2, + 'W': [3, 4, 5] + } + } + } + } + """ + + self.graph = {} + self.alpha = alpha + self.beta = beta + self.evap = evap + self.w_max = 7 + + + def set_antnet_hyperparams(self, c1, c2, gamma): + self.c1 = c1 + self.c2 = c2 + self.gamma = gamma + + + def node_exists(self, node): + return True if node in self.graph else False + + + def edge_exists(self, source, destination): + if not self.node_exists(source) or not self.node_exists(destination): + return False + return True if destination in self.graph[source]['neighbors'].keys() else False + + + def add_node(self, node): + self.graph[node] = {'neighbors':{},'routing_table':{node: 0.0}, 'pheromones':{}} #Set of pheromones with the neighbors + + + """ + Check necessary nodes (source and destination) if not exist and create them. + Add an edge with the travel time. + """ + def add_edge(self, source, destination, travel_time): + if not self.node_exists(source): + self.add_node(source) + if not self.node_exists(destination): + self.add_node(destination) + self.graph[source]['neighbors'][destination] = travel_time + self.graph[source]['pheromones'][destination] = 1.0 #For every new edge, the pheromone value is initialized to 1 + self.graph[source]['routing_table'][destination] = 0.0 + self.graph[source]['traffic_stat'] = {} + self.graph[source]['visited'] = False + self.graph[destination]['visited'] = False + + + def get_all_nodes(self): + return list(self.graph.keys()) + + + def get_all_edges(self): + edges = [] + for source in self.graph: + for destination in self.graph[source]['neighbors']: + edges.append((source, destination, self.graph[source]['neighbors'][destination])) + return edges + + + def get_node(self, node): + if self.node_exists(node): + return self.graph[node] + return None + + + def get_neighbors(self, node): + neighbors = [] + if self.node_exists(node): + for neighbor in self.graph[node]['neighbors']: + neighbors.append(neighbor) + return neighbors + + + # Getting the pheromones vlaues of the edges from which node is the source + def get_pheromones(self, node): + if self.node_exists(node): + return list(self.graph[node]['pheromones'].values()) + return None + + + def get_travel_times(self, node): + if self.node_exists(node): + return list(self.graph[node]['neighbors'].values()) + return None + + + def get_alpha(self): + return self.alpha + + + def get_beta(self): + return self.beta + + def get_evaporation(self): + return self.evap + + + def get_edge_time(self, source, destination): + if not self.node_exists(source): + return None + if not self.node_exists(destination): + return None + + if destination in self.graph[source]['neighbors']: + return self.graph[source]['neighbors'][destination] + + return None + + def delete_node(self, node): + if self.node_exists(node): + for n in self.graph: + if node in self.graph[n]['neighbors']: + del self.graph[n]['neighbors'][node] + del self.graph[node] + + + def delete_edge(self, source, destination): + if self.edge_exists(source, destination): + del self.graph[source]['neighbors'][destination] + + + def update_travel_time(self, source, destination, new_travel_time): + if self.edge_exists(source, destination): + if new_travel_time <= 0: + new_travel_time = 1 + self.graph[source]['neighbors'][destination] = new_travel_time + + + def get_path_cost(self, path): + cost = 0 + for i in range(len(path)-1): + if self.edge_exists(path[i],path[i+1]): + cost += self.get_edge_time(path[i],path[i+1]) + else: + return float('inf') + return cost + + + def update_pheromones(self, source, destination): + if self.edge_exists(source, destination): + self.graph[source]['pheromones'][destination] += 1 + + + def display_graph(self): + for node in self.graph: + print("--> NODE {} <--".format(node)) + for neighbor in self.graph[node]['neighbors']: + cost = self.graph[node]['neighbors'][neighbor] + pheros = self.graph[node]['pheromones'][neighbor] + print("{} -> {} Cost: {}, Pheros: {}".format(node, neighbor, cost, pheros)) + + print("Traffic Statistics to reach Destination") + if 'traffic_stat' in self.graph[node]: + for dest in self.graph[node]['traffic_stat']: + W = self.graph[node]['traffic_stat'][dest]['W'] + mean = self.graph[node]['traffic_stat'][dest]['mean'] + var = self.graph[node]['traffic_stat'][dest]['var'] + print("|- W = {}".format(W)) + print("|- Mean = {}".format(mean)) + print("|- Variance = {}".format(var)) + print() + + print("Routing Table Information") + for dest in self.graph[node]['routing_table']: + print(" |- Prob. to reach {} = {}".format(dest, self.graph[node]['routing_table'][dest])) + + print("-"*50) + print("-"*50) + + def evaporate(self): + for node in self.graph: + for neighbor in self.graph[node]['pheromones']: + self.graph[node]['pheromones'][neighbor] = (1-self.evap)*self.graph[node]['pheromones'][neighbor] + + + def add_phero(self, source, destination): + self.graph[source]['pheromones'][destination] += 1.0/self.graph[source]['neighbors'][destination] + + + def update_graph(self, max_delta_time=2, update_probability=0.7): + ''' + max_delta_time: maximum allowed change in travel time of an edge (in positive or negative direction) + update_probability: probability that the travel time of an edge will change + ''' + for edge in self.get_all_edges(): + if random.random() <= update_probability: # update the edge + delta_time = random.choice([i for i in range(-max_delta_time,max_delta_time+1,1) if i!=0]) # Change the travel time by delta_time units + self.update_travel_time(edge[0], edge[1], edge[2]+delta_time) + + # updates the traffic_stat datastructure of the node + def update_traffic_stat(self, node, destination, neighbor, t): + # Update traffic status + if destination in self.graph[node]['traffic_stat']: + self.graph[node]['traffic_stat'][destination]['W'].append(t) + self.graph[node]['traffic_stat'][destination]['mean'] = sum(self.graph[node]['traffic_stat'][destination]['W']) / len(self.graph[node]['traffic_stat'][destination]['W']) + self.graph[node]['traffic_stat'][destination]['var'] = ((t - self.graph[node]['traffic_stat'][destination]['mean'])**2) / len(self.graph[node]['traffic_stat'][destination]['W']) + else: + self.graph[node]['traffic_stat'][destination] = { + 'W': [t], + 'mean': t, + 'var': 0 + } + + if len(self.graph[node]['traffic_stat'][destination]['W']) > self.w_max: + self.graph[node]['traffic_stat'][destination]['W'].pop(0) + + # Update routing table + t_best = min(self.graph[node]['traffic_stat'][destination]['W']) + first_term = self.c1 * (t_best / t) + + try: + conf = math.sqrt(1 - self.gamma) + W_max = len(self.graph[node]['traffic_stat'][destination]['W']) + t_sup = self.graph[node]['traffic_stat'][destination]['mean'] + (self.graph[node]['traffic_stat'][destination]['var'] / (conf * math.sqrt(W_max))) + second_term = self.c2 * ((t_sup - t_best) / ((t_sup - t_best) + (t - t_best))) + except ZeroDivisionError as e: + second_term = 0 + + r = first_term + second_term + + # print("r for {} with neighbor {} -> {}".format(node, neighbor, r)) + # print(first_term, second_term) + + self.graph[node]['routing_table'][neighbor] += r * (1 - self.graph[node]['routing_table'][neighbor]) + for n in self.graph[node]['routing_table']: + if n == neighbor: + continue + self.graph[node]['routing_table'][n] -= r * self.graph[node]['routing_table'][n] + + + def set_window_size(self, w_max): + self.w_max = w_max \ No newline at end of file diff --git a/showml/antnet/simulation.py b/showml/antnet/simulation.py new file mode 100644 index 0000000..5edc3e6 --- /dev/null +++ b/showml/antnet/simulation.py @@ -0,0 +1,34 @@ +from graph import Graph +from ant import Ant +from antnet import runAntNet + +# Sample graph with edge cost +G = Graph(0.9,0.1,0.2) + +G.add_edge('A','B', 2) +G.add_edge('B','C', 2) +G.add_edge('A','H', 2) +G.add_edge('H','G', 2) +G.add_edge('C','F', 1) +G.add_edge('F','G', 1) +G.add_edge('G','F', 1) +G.add_edge('F','C', 1) +G.add_edge('C','D', 10) +G.add_edge('E','D', 2) +G.add_edge('G','E', 2) + +Ant.graph = G + +source = 'A' +destination = 'D' +iterations = 20 +num_episodes = 5 + +print("Path taken by ants on each episode with their cost") +for episode in range(1, num_episodes+1): + antnet_path = runAntNet(G, source, destination, 0.6, 0.3, 0.7) # Replace with -> antnet_path = antnet(G, source, destination) + print(antnet_path) + ant_net_cost = G.get_path_cost(antnet_path) + print(ant_net_cost) + print() + \ No newline at end of file