def test_get_adjacency_matrix(self): json_graph = {"graph": {"A": ["B"], "B": []}} graph = Graph(input_graph=json.dumps(json_graph)) matrix = graph.get_adjacency_matrix() answer = np.array([[0, 1], [0, 0]]) np.testing.assert_equal(matrix, answer) self.assertEqual(answer.size, matrix.size)
def is_sparse(graph: Graph) -> bool: """ Checks if |E| <= |V^2| / 2 :param graph: :return: """ return graph.size() <= (graph.order()**2 / 2)
def test_is_complete(self): graph = Graph(input_graph=json.dumps(self.complete_graph)) self.assertTrue(is_complete(graph)) graph = Graph(input_graph=json.dumps(self.isolated_graph)) self.assertFalse(is_complete(graph)) json_graph = {"name": "", "graph": {"A": ["B"], "B": []}} graph = Graph(input_graph=json.dumps(json_graph)) self.assertFalse(is_complete(graph))
def test_read_graph_from_file(self): json_graph = {"label": "my graph", "graph": {"A": ["B"], "B": []}} with open(path.join(self.test_dir, 'test.txt'), 'w') as out_file: out_file.write(json.dumps(json_graph)) graph = Graph(input_file=str(path.join(self.test_dir, 'test.txt'))) self.assertEqual(json_graph["label"], graph.get_label()) self.assertEqual(False, graph.is_directed()) self.assertEqual(json_graph["graph"], graph.get_graph())
def test_density(self): graph = Graph(input_graph=json.dumps(self.connected_graph)) self.assertAlmostEqual(0.4666666666666667, density(graph)) graph = Graph(input_graph=json.dumps(self.complete_graph)) self.assertEqual(1.0, density(graph)) graph = Graph(input_graph=json.dumps(self.isolated_graph)) self.assertEqual(0.0, density(graph))
def get_complement(graph: Graph) -> Graph: """ If graph is represented as a matrix, invert that matrix :param graph: :return: inversion of graph """ adj = graph.get_adjacency_matrix() complement = invert(adj) return Graph(label=f"{graph.get_label()} complement", input_array=complement)
def test_save_to_json(self): answer = "{\"label\": \"my graph\", \"directed\": false," \ " \"graph\": {\"A\": [\"B\"], \"B\": []}}" json_graph = {"label": "my graph", "graph": {"A": ["B"], "B": []}} graph = Graph(input_graph=json.dumps(json_graph)) save_to_json(graph, self.test_dir) outfile = path.join(self.test_dir, graph.get_label() + ".json") with open(outfile) as dot_file: dot_lines = "".join(dot_file.readlines()) self.assertEqual(dot_lines, answer)
def density(graph: Graph) -> float: """ The graph density is defined as the ratio of the number of edges of a given graph, and the total number of edges, the graph could have. A dense graph is a graph G = (V, E) in which |E| = Θ(|V|^2) :param graph: :return: """ V = len(graph.vertices()) E = len(graph.edges()) return 2.0 * (E / (V**2 - V))
def test_complete_digraph(self): graph = Graph(input_graph=json.dumps(self.complete_digraph)) self.assertTrue(is_complete(graph)) json_graph = { "name": "", "directed": True, "graph": { "A": ["B"], "B": [] } } graph = Graph(input_graph=json.dumps(json_graph)) self.assertFalse(is_complete(graph))
def save_to_dot(graph: Graph, out_dir: str): """ :param graph: the graph to render in dot :param out_dir: the absolute path of the gv file to write :return: """ if not graph.is_directed(): dot = GraphViz(comment=graph.get_label()) for node in graph: dot.node(node, node) for edge in graph[node]: dot.edge(node, edge) dot.render(path.join(out_dir, f"{graph.get_label()}.gv"), view=False)
def test_str(self): answer = """my graph A -> B B -> 0""" json_graph = {"label": "my graph", "graph": {"A": ["B"], "B": []}} graph = Graph(input_graph=json.dumps(json_graph)) self.assertEqual(answer, str(graph))
def arrival_departure_dfs(graph: Graph, v: str, discovered: Dict[str, bool], arrival: Dict[str, int], departure: Dict[str, int], time: int) -> int: """ Method for DFS with arrival and departure times for each vertex O(V+E) -- E could be as big as V^2 :param graph: :param v: :param discovered: :param arrival: :param departure: :param time: should be initialized to -1 :return: """ time += 1 # when did we arrive at vertex 'v'? arrival[v] = time discovered[v] = True for n in graph.get_neighbors(v): if not discovered.get(n, False): time = arrival_departure_dfs(graph, n, discovered, arrival, departure, time) time += 1 # increment time and then set departure departure[v] = time return time
def test_save_to_graphviz(self): answer = """// my graph graph { A [label=A] A -- B B [label=B] } """ json_graph = {"label": "my graph", "graph": {"A": ["B"], "B": []}} graph = Graph(input_graph=json.dumps(json_graph)) save_to_dot(graph, self.test_dir) outfile = path.join(self.test_dir, graph.get_label() + ".gv") with open(outfile) as dot_file: dot_lines = "".join(dot_file.readlines()) self.assertEqual(dot_lines, answer)
def mark_visited(g: Graph, v: str, v_map: Dict[str, bool], t_sort_results: List[str]): v_map[v] = True for n in g.get_neighbors(v): if not v_map[n]: mark_visited(g, n, v_map, t_sort_results) t_sort_results.append(v)
def save_to_json(graph: Graph, out_dir): """ :param graph: the graph to write to json :param out_dir: the absolute path to the dir to write the file :return: """ g_dict = { "label": graph.get_label(), "directed": graph.is_directed(), "graph": graph.get_graph() } with open(path.join(out_dir, f"{graph.get_label()}.json"), 'w', encoding="utf8") as out: out.write(json.dumps(g_dict))
def test_is_dag(self): cycle_graph = { "directed": True, "graph": { "A": ["B"], "B": ["C", "D"], "C": [], "D": ["E", "A"], # cycle A -> B -> D -> A "E": [] } } graph = Graph(input_graph=json.dumps(cycle_graph)) self.assertFalse(is_dag(graph)) dag = cycle_graph # remove the cycle dag["graph"]["D"] = ["E"] graph2 = Graph(input_graph=json.dumps(dag)) self.assertTrue(is_dag(graph2))
def test_arrival_departure_dfs(self): disjoint_graph = { "graph": { "a": ["b", "c"], "b": [], "c": ["d", "e"], "d": ["b", "f"], "e": ["f"], "f": [], "g": ["h"], "h": [] }, "directed": True } graph = Graph(input_graph=json.dumps(disjoint_graph)) # list to store the arrival time of vertex arrival = {v: 0 for v in graph.vertices()} # list to store the departure time of vertex departure = {v: 0 for v in graph.vertices()} # mark all the vertices as not discovered discovered = {v: False for v in graph.vertices()} time = -1 for v in graph.vertices(): if not discovered[v]: time = arrival_departure_dfs(graph, v, discovered, arrival, departure, time) # pair up the arrival and departure times and ensure correct ordering result = list(zip(arrival.values(), departure.values())) expected_times = [(0, 11), (1, 2), (3, 10), (4, 7), (8, 9), (5, 6), (12, 15), (13, 14)] self.assertListEqual(expected_times, result)
def test_edges(self): json_graph = {"label": "my graph", "graph": {"A": ["B"], "B": []}} graph = Graph(input_graph=json.dumps(json_graph)) self.assertEqual(json_graph['label'], graph.get_label()) self.assertEqual(False, graph.is_directed()) self.assertEqual(json_graph['graph'], graph.get_graph()) self.assertEqual([Edge('A', 'B')], graph.edges())
def test_find_circuit(self): circuit_graph = { "directed": True, "graph": { "A": ["B"], "B": ["C"], "C": ["A"] # circuit A -> C -> B -> A } } graph = Graph(input_graph=json.dumps(circuit_graph)) circuit = find_circuit(graph) expected_circuit = ["A", "C", "B", "A"] self.assertListEqual(circuit, expected_circuit)
def is_complete(graph: Graph) -> bool: """ Checks that each vertex has V(V-1) / 2 edges and that each vertex is connected to V - 1 others. runtime: O(n^2) :param graph: :return: true or false """ V = len(graph.vertices()) max_edges = (V**2 - V) if not graph.is_directed(): max_edges //= 2 E = len(graph.edges()) if E != max_edges: return False for vertex in graph: if len(graph[vertex]) != V - 1: return False return True
def check_for_cycles(graph: Graph, v: str, visited: Dict[str, bool], rec_stack: List[bool]) -> bool: """ :param graph: :param v: vertex to start from :param visited: list of visited vertices :param rec_stack: :return: whether or not the graph contains a cycle """ visited[v] = True rec_stack[graph.vertices().index(v)] = True for neighbour in graph[v]: if not visited.get(neighbour, False): if check_for_cycles(graph, neighbour, visited, rec_stack): return True elif rec_stack[graph.vertices().index(neighbour)]: return True rec_stack[graph.vertices().index(v)] = False return False
def is_simple(graph: Graph) -> bool: """ A simple graph has no cycles :param graph: :return: whether or not the graph is simple """ visited = {k: False for k in graph} rec_stack = [False] * graph.order() for v in graph: if not visited[v]: if check_for_cycles(graph, v, visited, rec_stack): return False return True
def topological(graph: Graph) -> List[str]: """ O(V+E) :param graph: :return: List of vertices sorted topologically """ def mark_visited(g: Graph, v: str, v_map: Dict[str, bool], t_sort_results: List[str]): v_map[v] = True for n in g.get_neighbors(v): if not v_map[n]: mark_visited(g, n, v_map, t_sort_results) t_sort_results.append(v) visited = {v: False for v in graph.vertices()} result: List[str] = [] for v in graph.vertices(): if not visited[v]: mark_visited(graph, v, visited, result) # Contains topo sort results in reverse order return result[::-1]
def test_topological_sort(self): t_sort_graph = { "directed": True, "graph": { "A": [], "B": [], "C": ["D"], "D": ["B"], "E": ["A", "B"], "F": ["A", "C"] } } graph = Graph(input_graph=json.dumps(t_sort_graph)) expected_results = ["F", "E", "C", "D", "B", "A"] results = topological(graph) self.assertListEqual(expected_results, results)
def diameter(graph: Graph) -> int: """ :param graph: :return: length of longest path in graph """ vee = graph.vertices() pairs = [(vee[i], vee[j]) for i in range(len(vee) - 1) for j in range(i + 1, len(vee))] smallest_paths = [] for (start, end) in pairs: paths = find_all_paths(graph, start, end) smallest = sorted(paths, key=len)[0] smallest_paths.append(smallest) smallest_paths.sort(key=len) # longest path is at the end of list, # i.e. diameter corresponds to the length of this path dia = len(smallest_paths[-1]) - 1 return dia
def test_find_all_paths(self): json_graph = { "label": "test2", "directed": False, "graph": { "a": ["d", "f"], "b": ["c"], "c": ["b", "c", "d", "e"], "d": ["a", "c"], "e": ["c"], "f": ["d"] } } graph = Graph(input_graph=json.dumps(json_graph)) paths = find_all_paths(graph, "a", "b") self.assertListEqual([['a', 'd', 'c', 'b'], ['a', 'f', 'd', 'c', 'b']], paths) paths = find_all_paths(graph, "z", "b") self.assertListEqual([], paths)
def test_iterator(self): json_graph = { "name": "my graph", "graph": { "A": ["B", "C", "D"], "B": [], "C": [], "D": [] } } graph = Graph(input_graph=json.dumps(json_graph)) iterations = 0 for key in graph: iterations += 1 if key == "A": self.assertEqual(len(graph[key]), 3) else: self.assertEqual(len(graph[key]), 0) self.assertEqual(iterations, 4)
def test_find_path(self): json_graph = { "label": "test", "directed": False, "graph": { "a": ["d"], "b": ["c"], "c": ["b", "c", "d", "e"], "d": ["a", "c"], "e": ["c"], "f": [] } } graph = Graph(input_graph=json.dumps(json_graph)) path = find_path(graph, "a", "b") self.assertListEqual(['a', 'd', 'c', 'b'], path) path = find_path(graph, "a", "f") self.assertListEqual([], path) path = find_path(graph, "c", "c") self.assertListEqual(['c'], path) path = find_path(graph, "z", "a") self.assertListEqual([], path)
def breadth_first_search(graph: Graph, start: str) -> List[str]: """ :param graph: :param start: the vertex to start the traversal from :return: """ # Mark all the vertices as not visited visited = {k: False for k in graph.vertices()} # Mark the start vertices as visited and enqueue it visited[start] = True queue = [start] walk = [] while queue: cur = queue.pop(0) walk.append(cur) for i in graph[cur]: if not visited[i]: queue.append(i) visited[i] = True return walk
def is_connected(graph: Graph, start_vertex: str = None, vertices_encountered: Set[str] = None) -> bool: """ :param graph: :param start_vertex: :param vertices_encountered: :return: whether or not the graph is connected """ if vertices_encountered is None: vertices_encountered = set() vertices = graph.vertices() if not start_vertex: # choose a vertex from graph as a starting point start_vertex = vertices[0] vertices_encountered.add(start_vertex) if len(vertices_encountered) != len(vertices): for vertex in graph[start_vertex]: if vertex not in vertices_encountered: if is_connected(graph, vertex, vertices_encountered): return True else: return True return False