def test_getNode_getEdge(self): print("test_getNode_getEdge") graph = DiGraph() graph.add_node(0) graph.add_node(1) graph.add_edge(0, 1, 1) e = graph.getEdge(0, 1) self.assertIsNotNone(e) self.assertEqual(e.getSrc(), 0) self.assertEqual(e.getDest(), 1) self.assertEqual(e.getWeight(), 1) n = graph.getNode(0) self.assertIsNotNone(n) self.assertEqual(n.getKey(), 0)
def test_get_all_v(self): graph = DiGraph() for i in range(5): graph.add_node(i) graph.add_edge(0, 1, 1.2) graph.add_edge(1, 2, 1.2) graph.add_edge(2, 3, 1.2) graph.add_edge(3, 4, 1.2) n = graph.getNode(0) c = graph.get_all_v() z = c.keys() self.assertTrue(z.__contains__(n.getKey())) c.pop(n.getKey()) self.assertFalse(z.__contains__(n.getKey()))
class GraphAlgo(GraphAlgoInterface): """This class represents a Directed (positive) Weighted Graph Theory Algorithms including: 0. __init__() 1. get_graph(self) 2. load_from_json(self, file_name: str) 3. save_to_json(self, file_name: str) 4. shortest_path(self, id1: int, id2: int) 5. connected_component(self, id1: int) 6. connected_components(self) 7. plot_graph(self) graph-Is an abstract representation of a set of nodes and edge, each edge has a weight, it is possible to have a route from node to another node.""" def __init__(self, graph: GraphInterface = None): """ Graph builder :param graph: graph """ self.__graph = graph def get_graph(self) -> GraphInterface: """ :return: the directed graph on which the algorithm works on """ return self.__graph def load_from_json(self, file_name: str) -> bool: """ This method load a graph to this graph. if the file was successfully loaded - the underlying graph of this class will be changed (to the loaded one), in case the graph was not loaded the original graph should remain "as is". :param file_name: file :return: true or false """ self.__graph = DiGraph() try: with open(file_name, "r") as file: my_dict = json.load(file) for item in my_dict['Nodes']: pos = [] if "pos" in item: for p in item['pos'].split(','): pos.append(float(p)) else: pos = [0, 0, 0] self.__graph.add_node(item['id'], (pos[0], pos[1], pos[2])) for item in my_dict['Edges']: self.__graph.add_edge(item['src'], item['dest'], item['w']) return True except IOError: return False def save_to_json(self, file_name: str) -> bool: """ Saves this weighted (directed) graph to the given file name - in JSON format :param file_name: file :return: true or false """ try: with open(file_name, "w") as file: file.write(str(self.__graph)) return True except IOError: return False def shortest_path(self, id1: int, id2: int) -> (float, list): """ Returns the shortest path from node id1 to node id2 using Dijkstra's Algorithm - as an ordered List of nodes: src--> n1-->n2-->...dest. By pass the shortest path from the end to the beginning. if no such path exists return math.inf, [ ](Maximum route length and empty list). The method adds the destination vertex (id2) to the list and accesses the parent vertex by the info value of the vertex until it arrives node that its info value is -1 which we set as the end of the trajectory. We will then create a list that will contain the weight of the destination node (id2) and the list of vertices of the shortest route created :param id1: src :param id2: dest :return: list of shortest path """ if id1 not in self.__graph.vertices or id2 not in self.__graph.vertices: pathInf = (math.inf, []) return pathInf self.Dijkstra(id1) distance = self.__graph.vertices.get(id2).getWeight() if distance == math.inf: pathInf = (distance, []) return pathInf path_list = [] if id1 == id2: path_list.insert(0, id2) distance = 0 listSp = (distance, path_list) return listSp else: path_list.insert(0, id2) parent = self.__graph.getNode(id2) while parent.getInfo() != -1: path_list.insert(0, parent.getInfo()) parent = self.__graph.getNode(parent.getInfo()) listSp = distance, path_list return listSp def connected_component(self, id1: int) -> list: """ This method gets a vertex Finds the Strongly Connected Component (SCC) that node id1 is a part of. If the node does not exist in the graph another blank list will be returned A list will be returned that will contain the nodes keys that are part of the bindings component. This method creates 3 lists: neiOut- contains the vertices that can be reached from the source node and neiIn- which contains the vertices in the graph that can be reached from the source node. Another allNode list in which all vertices are transferred and sent to the Valentine list from the previous 2. We will first insert the source vertex into allNode and neiOut We will reset the tag values of all the vertices of the graph-1 and for the source vertex we have a value of tag 0 then we will add all the vertices that can be reached from the source node to neiOut by going over the neighbors of each vertex entering allNode and defining each vertex entering a tag 0 so we do not repeat the operation From one time. We will perform these operations as long as the allNode list is not empty We will perform the same operation in the opposite direction, we will add to allNode all the codecs that can be reached from the source node by going over the neighbors' neighbors and thus we will fill in the neiIn list. Once these 2 lists are complete we will create a list that will contain the vertices that belong to neiIn and also neiOut which will return to us the Strongly Connected Component. :param id1: src :return: list of Strongly Connected Component """ if self.__graph.vertices.get(id1) is None: list_none = [] return list_none neiIn = [] neiOut = [] allNode = [self.__graph.getNode(id1)] neiOut.append(self.__graph.getNode(id1)) for n in self.__graph.get_all_v().keys(): temp = self.__graph.vertices.get(n) temp.setTag(-1) while len(allNode) > 0: prev = allNode.pop() prev.setTag(0) for nOut in self.__graph.neighborsOut.get(prev.getKey()).keys(): temp1 = self.__graph.vertices.get(nOut) if temp1.getTag() != 0: allNode.append(temp1) neiOut.append(temp1) temp1.setTag(0) for n in self.__graph.get_all_v().keys(): temp = self.__graph.vertices.get(n) temp.setTag(-1) allNode.append(self.__graph.getNode(id1)) neiIn.append(self.__graph.getNode(id1)) while len(allNode) > 0: prev = allNode.pop() prev.setTag(0) for nIn in self.__graph.neighborsIn.get(prev.getKey()).keys(): temp2 = self.__graph.vertices.get(nIn) if temp2.getTag() != 0: allNode.append(temp2) neiIn.append(temp2) temp2.setTag(0) for node in neiOut: for node2 in neiIn: if node.getKey() == node2.getKey(): allNode.append(node.getKey()) break return allNode def connected_components(self) -> List[list]: """ Finds all the Strongly Connected Component(SCC) in the graph. If there are no binding elements in the graph, [[]]] will return a list of empty lists. In the method we put all the keys in the graph in the allV list. We then sent a random vertex to the connected_component method () The list of the linking component we received will be added to the allComponents list And the vertices component bindings we got from the method we will remove from the allV list. We will do this as long as our list of vertices is not empty. And finally we return the allComponents list that contains all the existing binding elements in the graph. :return: list of list of Strongly Connected Components """ if self.__graph.v_size() < 1: return [[]] allComponents = [] allV = [] for k in self.__graph.get_all_v().keys(): allV.append(k) while len(allV) > 0: temp = allV[0] oneComponent = self.connected_component(temp) allComponents.append(oneComponent) for v in oneComponent: allV.remove(v) return allComponents def plot_graph(self) -> None: """ Plots the graph. If the nodes have a position, the nodes will be placed there. Otherwise, they will be placed in a random but elegant manner. The method draws the graph using the PlotGraph class (An explanation of the methods can be found in the imitation) Which has methods that create the graph using the "matplotlib" directory """ plot = PlotGraph(self.__graph) plot.have_pos() if plot.have_pos() is False: plot.random_pos() plot.paint() def Dijkstra(self, node_id: int): """ Algorithm for finding the shortest route with the help of a priority queue We will first go through all the nodes in the graph and define their math.inf weight Value info -1 We will insert the given vertex into the queue and as long as the queue is not empty we will perform the following steps: We will delete the vertex at the top of the queue and pass over all the neighbors of the same vertex and define a weight for them using the weight of the parent node and the connecting side between them so that we pass over all the vertices in the graph (if it is a different link only to some of them) And we will mark their weight a method which will help us find the shortest route. :param node_id: src """ queue = PriorityQueue() for node in self.__graph.vertices.values(): node.setWeight(math.inf) node.setInfo(-1) src = self.__graph.vertices.get(node_id) src.setWeight(0) queue.insert(src) while not queue.isEmpty(): prev = queue.delete() for k, w in self.__graph.all_out_edges_of_node( prev.getKey()).items(): dest = self.__graph.vertices.get(k) if dest.getWeight() > prev.getWeight() + w: dest.setWeight(prev.getWeight() + w) queue.insert(dest) dest.setInfo(prev.getKey())
class GraphAlgo(GraphAlgoInterface): def __init__(self, graph=None): if graph is None: self.directed_weighted_graph = DiGraph() else: self.directed_weighted_graph = graph self.parent = {} self.dis = {} def get_graph(self) -> GraphInterface: """ :return: the directed graph on which the algorithm works on. """ return self.directed_weighted_graph def parse_file_name(self, file_name): file_name = file_name.replace('/', '\\') file_name = file_name.replace('\\\\', '\\') file_name = file_name.split('\\') base_path = Path(__file__).parent.parent file_path = "" for i in range(len(file_name) - 1): if not file_name[i].startswith('..'): file_path += file_name[i] + '/' file_path = (base_path / file_path / file_name[len(file_name) - 1]).resolve() return file_path def load_from_json(self, file_name: str) -> bool: """ Loads a graph from a json file. @param file_name: The path to the json file @returns True if the loading was successful, False o.w. """ file_path = self.parse_file_name(file_name) with open(file_path, 'r') as fp: data = json.load(fp) nodes = data["Nodes"] edges = data["Edges"] for n in nodes: if "pos" in n: pos = n['pos'] if type(pos) == str: pos = tuple(pos.split(',')) pos = (float(pos[0]), float(pos[1])) self.directed_weighted_graph.add_node(n["id"], pos) else: self.directed_weighted_graph.add_node(n["id"]) for e in edges: self.directed_weighted_graph.add_edge(e["src"], e["dest"], e["w"]) return True def save_to_json(self, file_name: str) -> bool: """ Saves the graph in JSON format to a file @param file_name: The path to the out file @return: True if the save was successful, False o.w. """ file_name = self.parse_file_name(file_name) try: os.remove(file_name) except OSError: pass edges = self.directed_weighted_graph.get_edges() nodes = self.directed_weighted_graph.get_nodes() json_file = {} jsonEdges = [] jsonNodes = [] for src in edges: for dest in edges[src]: edge = edges[src][dest] parsed_edge = { 'src': edge.getSrc(), 'dest': edge.getDest(), 'w': edge.getWeight() } jsonEdges.append(parsed_edge) for k in nodes: if nodes[k].getLocation(): pos = nodes[k].getLocation() parsed_node = {'pos': pos, 'id': k} else: parsed_node = {'id': k} jsonNodes.append(parsed_node) json_file["Edges"] = jsonEdges json_file["Nodes"] = jsonNodes with open(file_name, 'x') as fp: json.dump(json_file, fp) return True def shortest_path_dist(self, src: int, dest: int) -> float: if src == dest: return 0 self.dijkstra(self.directed_weighted_graph.getNode(src)) return self.dis[dest] def shortest_path(self, id1: int, id2: int) -> (float, list): """ Returns the shortest path from node id1 to node id2 using Dijkstra's Algorithm @param id1: The start node id @param id2: The end node id @return: The distance of the path, a list of the nodes ids that the path goes through Example: # >>> from GraphAlgo import GraphAlgo # >>> g_algo = GraphAlgo() # >>> g_algo.addNode(0) # >>> g_algo.addNode(1) # >>> g_algo.addNode(2) # >>> g_algo.addEdge(0,1,1) # >>> g_algo.addEdge(1,2,4) # >>> g_algo.shortestPath(0,1) # (1, [0, 1]) # >>> g_algo.shortestPath(0,2) # (5, [0, 1, 2]) Notes: If there is no path between id1 and id2, or one of them dose not exist the function returns (float('inf'),[]) More info: https://en.wikipedia.org/wiki/Dijkstra's_algorithm """ if self.directed_weighted_graph.getNode( id1) is None or self.directed_weighted_graph.getNode( id2) is None: return None self.dijkstra(self.directed_weighted_graph.getNode(id1)) s = self.directed_weighted_graph.getNode(id2) path = [] while s is not None: path.append(s.getKey()) s = self.parent[s.getKey()] path.reverse() weight = self.dis[id2] if weight == float('inf'): return weight, [] return weight, path """ function Dijkstra(Graph, source): create vertex set Q for each vertex v in Graph: // Initialization dist[v] ← INFINITY // Unknown distance from source to v prev[v] ← UNDEFINED // Previous node in optimal path from source add v to Q // All nodes initially in Q (unvisited nodes) dist[source] ← 0 // Distance from source to source while Q is not empty: u ← vertex in Q with min dist[u] // Node with the least distance will be selected first remove u from Q for each neighbor v of u: // where v is still in Q. alt ← dist[u] + length(u, v) if alt < dist[v]: // A shorter path to v has been found dist[v] ← alt prev[v] ← u return dist[], prev[] """ def dijkstra(self, src): self.dis = {} self.parent = {} visited = set() q = [] for n in self.directed_weighted_graph.get_all_v(): self.dis[n.getKey()] = float('inf') self.parent[n.getKey()] = None self.dis[src.getKey()] = float(0) q.append((src.getKey(), 0)) while len(q) > 0 and len( visited) != self.directed_weighted_graph.v_size(): key = q.pop(0)[0] if key not in visited: for v in self.directed_weighted_graph.getNode( key).getOutEdges(): if v.getDest() not in visited: if self.directed_weighted_graph.getEdge( key, v.getDest()) is not None: tempSum = self.dis[ key] + self.directed_weighted_graph.getEdge( key, v.getDest()).getWeight() if tempSum < self.dis[v.getDest()]: self.dis[v.getDest()] = tempSum self.parent[v.getDest( )] = self.directed_weighted_graph.getNode(key) q.append((v.getDest(), self.dis[v.getDest()])) sorted(q, key=lambda n: n[1]) visited.add(key) def connected_component(self, id1: int) -> list: """ Finds the Strongly Connected Component(SCC) that node id1 is a part of. @param id1: The node id @return: The list of nodes in the SCC Notes: If the graph is None or id1 is not in the graph, the function should return an empty list [] """ list = [] if self.directed_weighted_graph.getNode(id1) is None: return list list.append(id1) for n in self.directed_weighted_graph.get_all_v(): if self.shortest_path( n.getKey(), id1)[0] != float('inf') and self.shortest_path( id1, n.getKey())[0] != float('inf'): if n.getKey() is not id1: list.append(n.getKey()) return list def connected_components(self) -> List[list]: """ Finds all the Strongly Connected Component(SCC) in the graph. @return: The list all SCC Notes: If the graph is None the function should return an empty list [] """ lists = [] if self.directed_weighted_graph is None: return lists for n in self.directed_weighted_graph.get_all_v(): lists.append(self.connected_component(n.getKey())) return lists def plot_graph(self) -> None: """ Plots the graph. If the nodes have a position, the nodes will be placed there. Otherwise, they will be placed in a random but elegant manner. @return: None """ ax = plt.axes() min_x = float('inf') max_x = -float('inf') min_y = float('inf') max_y = -float('inf') for node in self.directed_weighted_graph.get_nodes(): mynode = self.directed_weighted_graph.getNode(node) node_pos = mynode.getLocation() if node_pos is not None: if node_pos[0] < min_x: min_x = node_pos[0] if node_pos[1] < min_y: min_y = node_pos[1] if node_pos[0] > max_x: max_x = node_pos[0] if node_pos[1] > max_y: max_y = node_pos[1] else: min_x, max_x, min_y, max_y = 0, 500, 0, 500 plt.xlim(min_x + (min_x * 0.59), max_x + (max_x * 0.59)) plt.ylim(min_y + (min_y * 0.59), (max_y * 0.59) + max_y) edges = self.directed_weighted_graph.get_edges() plotted = [] circles = [] for src in edges: src_node = self.directed_weighted_graph.getNode(src) for dest in edges[src]: dest_node = self.directed_weighted_graph.getNode(dest) src_pos = src_node.getLocation() dest_pos = dest_node.getLocation() if src_pos is None: src_pos = [ random.uniform(min_x, max_x), random.uniform(min_y, max_y) ] src_node.setLocation(src_pos[0], src_pos[1]) if dest_pos is None: dest_pos = [ random.uniform(min_x, max_x), random.uniform(min_y, max_y) ] dest_node.setLocation(dest_pos[0], dest_pos[1]) c1 = plt.Circle((src_pos[0], src_pos[1]), max_x / 100 + max_y / 100, color='r') c2 = plt.Circle((dest_pos[0], dest_pos[1]), max_x / 100 + max_y / 100, color='r') if c1 not in circles: circles.append(c1) if c2 not in circles: circles.append(c2) if (src_pos[0] == dest_pos[0] and src_pos[1] == dest_pos[1]): dest_pos = (dest_pos[0] + 0.1, dest_pos[1] + 0.1) plt.arrow(src_pos[0], src_pos[1], dest_pos[0] - src_pos[0], dest_pos[1] - src_pos[1], head_width=max_x * 0.039, length_includes_head=True, head_length=max_y * 0.039, width=max_y * 0.00002 * max_y, color='black', fc="tan") plt.title('|V|=' + str(self.directed_weighted_graph.v_size()) + ',' + '|E|= ' + str(self.directed_weighted_graph.e_size()) + ')', fontdict={ 'color': 'white', 'fontsize': 19, 'fontweight': 980 }) if (src_pos[0], src_pos[1]) not in plotted: label = ax.annotate(src_node.getKey(), xy=(src_pos[0], src_pos[1]), fontsize=15) plotted.append([src_pos[0], src_pos[1]]) if (dest_pos[0], dest_pos[1]) not in plotted: label = ax.annotate(dest_node.getKey(), xy=(dest_pos[0], dest_pos[1]), fontsize=15) plotted.append([dest_pos[0], dest_pos[1]]) for i in circles: ax.add_artist(i) plt.show()