Graph Convolutional Branch and Bound v1.0.0
A TSP solver that combines a graph convolutional network with a 1-Tree branch-and-bound.
Loading...
Searching...
No Matches
HybridSolver.py
Go to the documentation of this file.
1"""
2 @file: HybridSolver.py
3 @author Lorenzo Sciandra
4 @brief First it builds the program in C, specifying the number of nodes to use and whether it is in hybrid mode or not.
5 Then it runs the graph conv net on the instance, and finally it runs the Branch and Bound.
6 It can be run on a single instance or a range of instances.
7 The input matrix is generated by the neural network and stored in the data folder. The output is stored in the results folder.
8 @version 1.0.0
9 @data 2024-05-1
10 @copyright Copyright (c) 2024, license MIT
11
12 Repo: https://github.com/LorenzoSciandra/GraphConvolutionalBranchandBound
13"""
14import subprocess
15import argparse
16import pprint as pp
17import os
18import time
19import numpy as np
20
21
22def adjacency_matrix(orig_graph):
23 """
24 Calculates the adjacency matrix of the graph.
25 Args:
26 orig_graph: The original graph.
27
28 Returns:
29 The adjacency matrix of the graph.
30 """
31 adj_matrix = np.zeros((len(orig_graph), len(orig_graph)))
32
33 for i in range(0, len(orig_graph)):
34 for j in range(i + 1, len(orig_graph)):
35 adj_matrix[i][j] = np.linalg.norm(np.array(orig_graph[i]) - np.array(orig_graph[j]))
36 adj_matrix[j][i] = adj_matrix[i][j]
37
38 return adj_matrix
39
40
41def create_temp_file(num_nodes, str_grap):
42 """
43 Creates a temporary file to store the current instance of the TSP for the neural network.
44 Args:
45 num_nodes: The number of nodes in the TSP instance.
46 str_grap: The string representation of the graph.
47 """
48 filepath = "graph-convnet-tsp/data/hyb_tsp/test_" + str(num_nodes) + "_nodes_temp.txt"
49
50 if not os.path.exists(os.path.dirname(filepath)):
51 try:
52 os.makedirs(os.path.dirname(filepath))
53 except OSError as exc: # Guard against race condition
54 if exc.errno != errno.EEXIST:
55 raise
56
57 with open(filepath, 'w+') as file:
58 file.writelines(str_grap)
59 file.flush()
60 os.fsync(file.fileno())
61
62
63def get_nodes(graph):
64 """
65 From a graph, it returns the nodes in a string format.
66 Args:
67 graph: The graph to get the nodes from.
68
69 Returns:
70 The nodes in a string format.
71 """
72 nodes = ""
73 i = 0
74 for node in graph:
75 nodes += "\t" + str(i) + " : " + str(node[0]) + " " + str(node[1]) + "\n"
76 i += 1
77
78 return nodes
79
80
81def get_instance(instance, num_nodes):
82 """
83 Gets the instance of the TSP from the file.
84 Args:
85 instance: The instance to get.
86 num_nodes: The number of nodes in the TSP instance.
87
88 Returns:
89 The graph in a list and string format.
90 """
91
92 lines = None
93 file_path = "graph-convnet-tsp/data/hyb_tsp/test_" + str(num_nodes) + "_nodes.txt"
94 with open(file_path, "r") as f:
95 lines = f.readlines()
96
97 if lines is None or len(lines) < instance - 1:
98 raise Exception(
99 "The instance " + str(instance) + " for the number of nodes " + str(num_nodes) + " does not exist.")
100
101 str_graph = lines[instance - 1]
102 orig_graph = str_graph
103
104 if "output" in str_graph:
105 str_graph = str_graph.split(" output")[0]
106
107 str_graph = str_graph.replace("\n", "").strip()
108 nodes = str_graph.split(" ")
109 graph = [float(x) for x in nodes]
110 graph = [[graph[i], graph[i + 1]] for i in range(0, len(graph), 2)]
111
112 return graph, orig_graph
113
114
115def build_c_program(build_directory, num_nodes, hyb_mode):
116 """
117 Builds the C program with the specified number of nodes and whether it is in hybrid mode or not.
118 Args:
119 build_directory: The directory where the CMakeLists.txt file is located and where the executable will be built.
120 num_nodes: The number of nodes to use in the C program.
121 hyb_mode: 1 if the program is in hybrid mode, 0 otherwise.
122 """
123 source_directory = "../"
124 cmake_command = [
125 "cmake",
126 "-S" + source_directory,
127 "-B" + build_directory,
128 "-DCMAKE_BUILD_TYPE=Release",
129 "-DMAX_VERTEX_NUM=" + str(num_nodes),
130 "-DHYBRID=" + str(hyb_mode)
131 ]
132 # print(cmake_command)
133 make_command = [
134 "make",
135 "-C" + build_directory,
136 "-j"
137 ]
138 try:
139 subprocess.check_call(cmake_command)
140 subprocess.check_call(make_command)
141 except subprocess.CalledProcessError as e:
142 print("Build failed:")
143 print(e.output)
144 raise Exception("Build failed")
145
146
147def hybrid_solver(num_instances, num_nodes, hyb_mode, gen_matrix):
148 """
149 The Graph Convolutional Branch-and-Bound Solver.
150 Args:
151 num_instances: The range of instances to run on the Solver.
152 num_nodes: The number of nodes in each TSP instance.
153 hyb_mode: True if the program is in hybrid mode, False otherwise.
154 gen_matrix: True if the adjacency matrix is already generated, False otherwise.
155 """
156
157 model_size = 0
158 adj_matrix = None
159
160 if hyb_mode:
161 if num_nodes <= 1:
162 raise Exception("The number of nodes must be greater than 1.")
163 elif num_nodes <= 20:
164 model_size = 20
165 elif num_nodes <= 50:
166 model_size = 50
167 else:
168 model_size = 100
169 else:
170 model_size = num_nodes
171
172 build_directory = "../cmake-build/CMakeFiles/BranchAndBound1Tree.dir"
173 hybrid = 1 if hyb_mode else 0
174 build_c_program(build_directory, num_nodes, hybrid)
175
176 if "-" in num_instances:
177 instances = num_instances.split("-")
178 start_instance = 1 if int(instances[0]) == 0 else int(instances[0])
179 end_instance = int(instances[1])
180 else:
181 start_instance = 1
182 end_instance = int(num_instances)
183
184 print("Starting instance: " + str(start_instance))
185 print("Ending instance: " + str(end_instance))
186
187 for i in range(start_instance, end_instance + 1):
188 start_time = time.time()
189 graph, str_graph = get_instance(i, num_nodes)
190 input_file = "../data/AdjacencyMatrix/tsp_" + str(num_nodes) + "_nodes/tsp_test_" + str(i) + ".csv"
191 absolute_input_path = os.path.abspath(input_file)
192
193 if not os.path.exists(os.path.dirname(absolute_input_path)):
194 try:
195 os.makedirs(os.path.dirname(absolute_input_path))
196 except OSError as exc: # Guard against race condition
197 if exc.errno != errno.EEXIST:
198 raise
199
200 result_mode = "hybrid" if hyb_mode else "classic"
201 output_file = "../results/AdjacencyMatrix/tsp_" + str(num_nodes) + "_nodes_" + result_mode \
202 + "/tsp_result_" + str(i) + ".txt"
203
204 if hyb_mode:
205 create_temp_file(num_nodes, str_graph)
206 absolute_python_path = os.path.abspath("./graph-convnet-tsp/main.py")
207 result = subprocess.run(
208 ['python3', absolute_python_path, absolute_input_path, str(num_nodes), str(model_size)],
209 cwd="./graph-convnet-tsp", check=True)
210 if result.returncode == 0:
211 print('Neural Network completed successfully on instance ' + str(i) + ' / ' + str(end_instance))
212 else:
213 print('Neural Network failed on instance ' + str(i) + ' / ' + str(end_instance))
214
215 elif gen_matrix:
216 adj_matrix = adjacency_matrix(graph)
217 with open(absolute_input_path, "w") as f:
218 nodes_coord = ";".join([f"({graph[i][0]}, {graph[i][1]})" for i in range(len(graph))])
219 f.write(nodes_coord + "\n")
220 for k in range(len(adj_matrix)):
221 for j in range(len(adj_matrix[k])):
222 f.write(f"({adj_matrix[k][j]}, 0);")
223 f.write("\n")
224
225 absolute_output_path = os.path.abspath(output_file)
226 if not os.path.exists(os.path.dirname(absolute_output_path)):
227 try:
228 os.makedirs(os.path.dirname(absolute_output_path))
229 except OSError as exc: # Guard against race condition
230 if exc.errno != errno.EEXIST:
231 raise
232 cmd = [build_directory + "/BranchAndBound1Tree", absolute_input_path, absolute_output_path]
233 result = subprocess.run(cmd)
234 if result.returncode == 0:
235 print('Branch-and-Bound completed successfully on instance ' + str(i) + ' / ' + str(end_instance))
236 else:
237 print('Branch-and-Bound failed on instance ' + str(i) + ' / ' + str(end_instance))
238
239 end_time = time.time()
240 cities = get_nodes(graph)
241
242 if hyb_mode:
243 os.remove("graph-convnet-tsp/data/hyb_tsp/test_" + str(num_nodes) + "_nodes_temp.txt")
244
245 with open(output_file, "a") as f:
246 f.write("\nNodes: \n" + cities)
247 f.write("\nTime taken: " + str(end_time - start_time) + "s\n")
248 f.flush()
249 os.fsync(f.fileno())
250
251
252if __name__ == "__main__":
253 """
254 Args:
255 --range_instances: The range of instances to run on the Solver.
256 --num_nodes: The number of nodes in each TSP instance.
257 --hybrid_mode: If present, the program is in hybrid mode, otherwise it is in classic mode.
258 """
259
260 parser = argparse.ArgumentParser()
261 parser.add_argument("--range_instances", type=str, default="1-1")
262 parser.add_argument("--num_nodes", type=int, default=20)
263 parser.add_argument("--hybrid", action="store_true")
264 opts = parser.parse_args()
265
266 pp.pprint(vars(opts))
267
268 gen_matrix = opts.hybrid == False
269
270 hybrid_solver(opts.range_instances, opts.num_nodes, opts.hybrid, gen_matrix)
def get_instance(instance, num_nodes)
Definition: HybridSolver.py:81
def get_nodes(graph)
Definition: HybridSolver.py:63
def hybrid_solver(num_instances, num_nodes, hyb_mode, gen_matrix)
def build_c_program(build_directory, num_nodes, hyb_mode)
def create_temp_file(num_nodes, str_grap)
Definition: HybridSolver.py:41
def adjacency_matrix(orig_graph)
Definition: HybridSolver.py:22