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
main.py
Go to the documentation of this file.
1"""
2 @file main.py
3 @author Lorenzo Sciandra
4 @brief A recombination of code take from: https://github.com/chaitjo/graph-convnet-tsp.
5 Some functions were created for the purpose of the paper.
6 @version 1.0.0
7 @data 2024-05-1
8 @copyright Copyright (c) 2024, license MIT
9 Repo: https://github.com/LorenzoSciandra/GraphConvolutionalBranchandBound
10"""
11import errno
12import os
13import sys
14import time
15import numpy as np
16import torch
17from torch.autograd import Variable
18import torch.nn.functional as F
19import torch.nn as nn
20from sklearn.utils.class_weight import compute_class_weight
21# Remove warning
22import warnings
23
24warnings.filterwarnings("ignore", category=UserWarning)
25from scipy.sparse import SparseEfficiencyWarning
26
27warnings.simplefilter('ignore', SparseEfficiencyWarning)
28from config import *
29from utils.graph_utils import *
30from utils.google_tsp_reader import GoogleTSPReader
31from utils.plot_utils import *
32from models.gcn_model import ResidualGatedGCNModel
33from sklearn_extra.cluster import KMedoids
34from utils.model_utils import *
35
36
37def compute_prob(net, config, dtypeLong, dtypeFloat):
38 """
39 This function computes the probability of the edges being in the optimal tour, by running the GCN.
40 Args:
41 net: The Graph Convolutional Network.
42 config: The configuration file, from which the parameters are taken.
43 dtypeLong: The data type for the long tensors.
44 dtypeFloat: The data type for the float tensors.
45 Returns:
46 y_probs: The probability of the edges being in the optimal tour.
47 x_edges_values: The distance between the nodes.
48 """
49 # Set evaluation mode
50 net.eval()
51
52 # Assign parameters
53 num_nodes = config.num_nodes
54 num_neighbors = config.num_neighbors
55 batch_size = config.batch_size
56 test_filepath = config.test_filepath
57
58 # Load TSP data
59 dataset = GoogleTSPReader(num_nodes, num_neighbors, batch_size=batch_size, filepath=test_filepath)
60
61 # Convert dataset to iterable
62 dataset = iter(dataset)
63
64 # Initially set loss class weights as None
65 edge_cw = None
66
67 y_probs = []
68
69 # read the instance number line from the test_filepath
70 instance = None
71 lines = []
72 with open(test_filepath, 'r') as f:
73 lines = f.readlines()
74
75 if lines is None:
76 raise Exception("The input file is empty.")
77
78 instance = lines[0]
79
80 if instance is None:
81 raise Exception("The instance does not exist.")
82
83 instance = instance.split(" output")[0]
84 instance = [float(x) for x in instance.split(" ")]
85 # print(config)
86
87 with torch.no_grad():
88
89 batch = next(dataset)
90
91 while batch.nodes_coord.flatten().tolist() != instance:
92 batch = next(dataset)
93
94 x_edges = Variable(torch.LongTensor(batch.edges).type(dtypeLong), requires_grad=False)
95 x_edges_values = Variable(torch.FloatTensor(batch.edges_values).type(dtypeFloat), requires_grad=False)
96 x_nodes = Variable(torch.LongTensor(batch.nodes).type(dtypeLong), requires_grad=False)
97 x_nodes_coord = Variable(torch.FloatTensor(batch.nodes_coord).type(dtypeFloat), requires_grad=False)
98 y_edges = Variable(torch.LongTensor(batch.edges_target).type(dtypeLong), requires_grad=False)
99
100 # Compute class weights (if uncomputed)
101 if type(edge_cw) != torch.Tensor:
102 edge_labels = y_edges.cpu().numpy().flatten()
103 edge_cw = compute_class_weight("balanced", classes=np.unique(edge_labels), y=edge_labels)
104
105 y_preds, _ = net.forward(x_edges, x_edges_values, x_nodes, x_nodes_coord, y_edges, edge_cw)
106 y = F.softmax(y_preds, dim=3)
107 # y_bins = y.argmax(dim=3)
108 y_probs = y[:, :, :, 1]
109
110 nodes_coord = batch.nodes_coord.flatten().tolist()
111
112 return y_probs, x_edges_values, nodes_coord
113
114
115def write_adjacency_matrix(graph, y_probs, x_edges_values, nodes_coord, filepath, num_nodes, kmedoids_labels=None):
116 """
117 This function writes the adjacency matrix to a file.
118 The file is in the format:
119 cities: (x1, y1);(x2, y2);...;(xn, yn)
120 adjacency matrix:
121 (0.0, 0.0);(0.23, 0.9);...;(0.15, 0.56)
122 ...
123 (0.23, 0.9);(0.3, 0.59);...;(0.0, 0.0)
124 where each entry is (distance, probability)
125 If needed adjusts the size of the graph when the model size is different from the number of nodes in the instance.
126 Args:
127 graph: The set of nodes in the graph.
128 y_probs: The probability of the edges being in the optimal tour.
129 x_edges_values: The weight of the edges.
130 nodes_coord: The nodes coordinates used in the GCN.
131 filepath: The path to the file where the adjacency matrix will be written.
132 num_nodes: The number of nodes in the TSP instance.
133 kmedoids_labels: The labels of the k-medoids clustering.
134 """
135
136 model_size = y_probs.shape[1]
137 y_probs = y_probs.flatten().numpy()
138 x_edges_values = x_edges_values.flatten().numpy()
139
140 # stack the arrays horizontally and convert to string data type
141 arr_combined = np.stack((x_edges_values, y_probs), axis=1).astype('U')
142
143 if num_nodes < model_size:
144 nodes_coord = nodes_coord[:num_nodes*2]
145 final_arr = []
146 for i in range(num_nodes):
147 for j in range(num_nodes):
148 final_arr.append(arr_combined[i * model_size + j])
149
150 for j in range(num_nodes, model_size):
151 if i != j-num_nodes:
152 if arr_combined[i * model_size + j][1] > final_arr[i * num_nodes + j - num_nodes][1]:
153 final_arr[i * num_nodes + j - num_nodes][1] = arr_combined[i * model_size + j][1]
154
155 arr_combined = np.array(final_arr)
156
157 elif num_nodes > model_size:
158 nodes_coord = [[nodes_coord[i], nodes_coord[i + 1]] for i in range(0, len(nodes_coord), 2)]
159 arr_combined = arr_combined.flatten()
160 arr_combined = arr_combined.reshape(model_size, model_size, 2).tolist()
161 j = 0
162 for node in graph:
163 if node not in nodes_coord:
164 new_row = []
165 label = kmedoids_labels[j]
166
167 for i in range(len(nodes_coord)):
168 distance = np.linalg.norm(np.array(node) - np.array(nodes_coord[i]))
169 prob = 0.0 #arr_combined[label][i][1]
170 arr_combined[i].append([distance, prob])
171 new_row.append([distance, prob])
172
173 new_row.append([0.0, 0.0])
174 arr_combined.append(new_row)
175 nodes_coord.append(node)
176
177 j += 1
178
179 arr_combined = np.array(arr_combined).flatten().tolist()
180 arr_combined = [[arr_combined[i], arr_combined[i + 1]] for i in range(0, len(arr_combined), 2)]
181 nodes_coord = np.array(nodes_coord).flatten().tolist()
182
183 nodes_coord = ";".join([f"({nodes_coord[i]}, {nodes_coord[i + 1]})" for i in range(0, len(nodes_coord), 2)])
184 arr_strings = np.array(['({}, {});'.format(x[0], x[1]) for x in arr_combined])
185
186 with open(filepath, 'w') as f:
187 f.write("%s\n" % nodes_coord)
188 edge = 0
189 for item in arr_strings:
190 if (edge + 1) % num_nodes == 0:
191 f.write("%s\n" % item)
192 else:
193 f.write("%s" % item)
194 edge += 1
195 f.flush()
196 os.fsync(f.fileno())
197
198
199def add_dummy_cities(num_nodes, model_size):
200 """
201 This function adds dummy cities to the graph instance. The dummy cities are randomly generated are
202 added to the graph instance and the new instance is saved in a temporary file.
203 Args:
204 num_nodes: The number of nodes of the graph instance.
205 model_size: The size of the Graph Convolutional Network to use.
206 """
207
208 num_dummy_cities = model_size - num_nodes
209 filepath = "data/hyb_tsp/test_" + str(num_nodes) + "_nodes_temp.txt"
210 graph_str = None
211
212 with open(filepath, "r") as f:
213 lines = f.readlines()
214 graph_str = lines[0]
215
216 graph_str = graph_str.split(" output")[0]
217 if graph_str is None:
218 raise Exception("The input file is empty.")
219
220 nodes = graph_str.split(" ")
221 graph = [[float(nodes[i]), float(nodes[i + 1])] for i in range(0, len(nodes), 2)]
222 rr_index = 0
223 for i in range(num_dummy_cities):
224 find = False
225 x = None
226 y = None
227 while not find:
228 values = np.random.randint(1, 9, 2)
229 signs = np.random.choice([-1, 1], 2)
230 x = graph[rr_index][0] + signs[0] * values[0] * 0.000000000000001
231 y = graph[rr_index][1] + signs[1] * values[1] * 0.000000000000001
232 if [x, y] not in graph:
233 find = True
234
235 graph.append([x, y])
236 graph_str += " " + str(x) + " " + str(y)
237 rr_index = (rr_index + 1) % num_nodes
238
239 seq = np.linspace(1,model_size, model_size, dtype=int)
240 seq_str = ""
241 for s in seq:
242 seq_str += str(s) + " "
243
244 seq_str += "1"
245 graph_str += " output " + seq_str
246
247 with open(filepath, 'w+') as file:
248 file.writelines(graph_str)
249 file.flush()
250 os.fsync(file.fileno())
251
252
253def create_temp_file(num_nodes, str_grap):
254 """
255 Creates a temporary file with the graph instance.
256 Args:
257 num_nodes: The number of nodes of the graph instance.
258 str_grap: The graph instance.
259 """
260
261 filepath = "data/hyb_tsp/test_" + str(num_nodes) + "_nodes_temp.txt"
262
263 with open(filepath, 'w+') as file:
264 file.writelines(str_grap)
265 file.flush()
266 os.fsync(file.fileno())
267
268
269def cluster_nodes(graph, k):
270 """
271 Applies the k-medoids clustering to the graph.
272 Args:
273 graph: The graph to cluster.
274 k: The number of clusters to create.
275 Returns:
276 medoids_str: The medoids of the clusters.
277 kmedoids.labels_: The labels of the clusters.
278 """
279 graph = np.array(graph)
280 kmedoids = KMedoids(n_clusters=k, method='pam', random_state=42).fit(graph)
281 medoids = kmedoids.cluster_centers_
282 medoids_str = " ".join(f"{x} {y}" for x, y in medoids)
283
284 return medoids_str, kmedoids.labels_
285
286
287def fix_instance_size(graph, num_nodes, model_size=100):
288 """
289 The function that fixes the instance size with clustering.
290 It applies the k-medoids clustering to the graph and creates a new instance with the medoids as the new nodes.
291 Args:
292 graph: The graph to fix.
293 num_nodes: The number of nodes of the graph instance.
294 model_size: The size of the Graph Convolutional Network to use.
295 """
296
297 new_graph_str = ""
298 end_str = " output "
299
300 print("Need to fix the instance size with clustering")
301 new_graph_str, kmedoids_labels = cluster_nodes(graph, model_size)
302
303 for i in range(1, model_size + 1):
304 end_str += str(i) + " "
305
306 end_str += "1"
307
308 create_temp_file(num_nodes, new_graph_str + end_str)
309 return kmedoids_labels
310
311
312def get_instance(num_nodes):
313 """
314 The function that reads the current instance from the file.
315 Args:
316 num_nodes: The number of nodes of the graph instance.
317 Returns:
318 graph: The graph instance.
319 """
320
321 lines = None
322 file_path = "data/hyb_tsp/test_" + str(num_nodes) + "_nodes_temp.txt"
323
324 with open(file_path, "r") as f:
325 lines = f.readlines()
326
327 if lines is None or len(lines) == 0:
328 raise Exception(
329 "The current instance for the number of nodes " + str(num_nodes) + " does not exist.")
330
331 str_graph = lines[0]
332
333 if "output" in str_graph:
334 str_graph = str_graph.split(" output")[0]
335
336 str_graph = str_graph.replace("\n", "").strip()
337 nodes = str_graph.split(" ")
338 graph = [float(x) for x in nodes]
339 graph = [[graph[i], graph[i + 1]] for i in range(0, len(graph), 2)]
340
341 return graph
342
343
344
345def main(filepath, num_nodes, model_size):
346 """
347 The function that runs the Graph Convolutional Network and writes the adjacency matrix to a file
348 for the given input instance.
349 Args:
350 filepath: The path to the file where the adjacency matrix will be written.
351 num_nodes: The number of nodes in the TSP instance.
352 model_size: The size of the Graph Convolutional Network to use.
353 """
354
355 graph = None
356 kmedoids_labels = None
357
358 if num_nodes < model_size:
359 add_dummy_cities(num_nodes, model_size)
360 elif num_nodes > model_size:
361 graph = get_instance(num_nodes)
362 kmedoids_labels = fix_instance_size(graph, num_nodes)
363
364 config_path = "./logs/tsp" + str(model_size) + "/config.json"
365 config = get_config(config_path)
366
367 config.gpu_id = "0"
368 config.accumulation_steps = 1
369
370 config.val_filepath = "data/hyb_tsp/test_" + str(num_nodes) + "_nodes_temp.txt"
371 config.test_filepath = "data/hyb_tsp/test_" + str(num_nodes) + "_nodes_temp.txt"
372
373 os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
374 os.environ["CUDA_VISIBLE_DEVICES"] = str(config.gpu_id)
375
376 if torch.cuda.is_available():
377 # print("CUDA available, using GPU ID {}".format(config.gpu_id))
378 dtypeFloat = torch.cuda.FloatTensor
379 dtypeLong = torch.cuda.LongTensor
380 torch.cuda.manual_seed(1)
381 else:
382 # print("CUDA not available")
383 dtypeFloat = torch.FloatTensor
384 dtypeLong = torch.LongTensor
385 torch.manual_seed(1)
386
387 net = nn.DataParallel(ResidualGatedGCNModel(config, dtypeFloat, dtypeLong))
388 if torch.cuda.is_available():
389 net.cuda()
390
391 log_dir = f"./logs/{config.expt_name}/"
392 if torch.cuda.is_available():
393 checkpoint = torch.load(log_dir + "best_val_checkpoint.tar")
394 else:
395 checkpoint = torch.load(log_dir + "best_val_checkpoint.tar", map_location='cpu')
396 # Load network state
397 net.load_state_dict(checkpoint['model_state_dict'])
398 config.batch_size = 1
399 probs, edges_value, nodes_coord = compute_prob(net, config, dtypeLong, dtypeFloat)
400 write_adjacency_matrix(graph, probs, edges_value, nodes_coord, filepath, num_nodes, kmedoids_labels)
401
402
403if __name__ == "__main__":
404 """
405 Args:
406 sys.argv[1]: The path to the file where the adjacency matrix will be written.
407 sys.argv[2]: The number of nodes in the TSP instance.
408 sys.argv[3]: The dimension of the model.
409 """
410
411 if len(sys.argv) != 4:
412 print("\nPlease provide the path to the output file to write in, the number of nodes in the tsp and the "
413 "instance number to analyze. The format is: "
414 "<filepath> <number of nodes> <model size>\n")
415 sys.exit(1)
416
417 if not isinstance(sys.argv[1], str) or not isinstance(sys.argv[2], str) or not isinstance(sys.argv[3], str):
418 print("Error: The arguments must be strings.")
419 sys.exit(1)
420
421 filepath = sys.argv[1]
422 num_nodes = int(sys.argv[2])
423 model_size = int(sys.argv[3])
424 main(filepath, num_nodes, model_size)
Definition: main.py:1
def compute_prob(net, config, dtypeLong, dtypeFloat)
Definition: main.py:37
def get_instance(num_nodes)
Definition: main.py:312
def add_dummy_cities(num_nodes, model_size)
Definition: main.py:199
def cluster_nodes(graph, k)
Definition: main.py:269
def write_adjacency_matrix(graph, y_probs, x_edges_values, nodes_coord, filepath, num_nodes, kmedoids_labels=None)
Definition: main.py:115
def fix_instance_size(graph, num_nodes, model_size=100)
Definition: main.py:287
def create_temp_file(num_nodes, str_grap)
Definition: main.py:253