Spaces:
Paused
Paused
| import torch.nn as nn | |
| import scipy | |
| import numpy as np | |
| import os | |
| import datetime | |
| import subprocess | |
| import models.solvers.concorde.concorde_utils as concorde_utils | |
| import glob | |
| import random | |
| class ConcordeTSP(nn.Module): | |
| def __init__(self, large_value=1e+6, scaling=False, random_seed=1234, solver_dir="models/solvers/concorde/src/TSP", io_dir="concorde_io_files"): | |
| self.random_seed = random_seed | |
| self.large_value = large_value | |
| self.scaling = scaling | |
| self.solver_dir = solver_dir | |
| self.io_dir = io_dir | |
| self.redirector_stdout = concorde_utils.Redirector(fd=concorde_utils.STDOUT) | |
| self.redirector_stderr = concorde_utils.Redirector(fd=concorde_utils.STDERR) | |
| os.makedirs(io_dir, exist_ok=True) | |
| def get_instance_name(self): | |
| now = datetime.datetime.now() | |
| random_value = random.random() # for avoiding duplicated file name | |
| instance_name = f"{os.getpid()}_{random_value}_{now.strftime('%Y%m%d_%H%M%S%f')}" | |
| return instance_name | |
| def write_instance(self, node_feats, fixed_paths=None, instance_name=None): | |
| if instance_name is None: | |
| instance_name = self.get_instance_name() | |
| instance_fname = f"{self.io_dir}/{instance_name}.tsp" | |
| tour_fname = f"{self.io_dir}/{instance_name}.sol" | |
| with open(instance_fname, "w") as f: | |
| f.write(f"NAME : {instance_name}\n") | |
| f.write(f"TYPE : TSP\n") | |
| f.write(f"DIMENSION : {len(node_feats['coords'])}\n") | |
| self.write_data(f, node_feats, fixed_paths) | |
| f.write("EOF\n") | |
| return instance_fname, tour_fname | |
| def write_data(self, f, node_feats, fixed_paths=None): | |
| coords = node_feats["coords"] | |
| if fixed_paths is None: | |
| f.write("EDGE_WEIGHT_TYPE : EUC_2D\n") | |
| f.write("NODE_COORD_SECTION\n") | |
| for i in range(len(coords)): | |
| f.write(f" {i + 1} {str(coords[i][0])[:10]} {str(coords[i][1])[:10]}\n") | |
| else: | |
| f.write("EDGE_WEIGHT_TYPE : EXPLICIT\n") | |
| f.write("EDGE_WEIGHT_FORMAT : FULL_MATRIX\n") | |
| f.write("EDGE_WEIGHT_SECTION\n") | |
| dist = scipy.spatial.distance.cdist(coords, coords).round().astype(np.int64) | |
| for i in range(len(fixed_paths)): | |
| curr_id = fixed_paths[i] | |
| if i != 0 and i != len(fixed_paths) - 1: | |
| # NOTE: concorde TSP seems to use int32, so 1e+9 occurs overflow. | |
| # 1e+8 could also do the same when N (tour length) is large. | |
| dist[curr_id, :] = 1e+8; dist[:, curr_id] = 1e+8 | |
| if i != 0: | |
| prev_id = fixed_paths[i - 1] | |
| dist[prev_id, curr_id] = 0; dist[curr_id, prev_id] = 0 | |
| if i != len(fixed_paths) - 1: | |
| next_id = fixed_paths[i + 1] | |
| dist[curr_id, next_id] = 0; dist[next_id, curr_id] = 0 | |
| f.write("\n".join([ | |
| " ".join(map(str, row)) | |
| for row in dist | |
| ])) | |
| def solve(self, node_feats, fixed_paths=None, instance_name=None): | |
| if self.scaling: | |
| node_feats = self.preprocess_data(node_feats) | |
| self.redirector_stdout.start() | |
| self.redirector_stderr.start() | |
| instance_fname, tour_fname = self.write_instance(node_feats, fixed_paths, instance_name) | |
| subprocess.run(f"{self.solver_dir}/concorde -o {tour_fname} -x {instance_fname}", shell=True) # run Concorde | |
| self.redirector_stderr.stop() | |
| self.redirector_stdout.stop() | |
| tours = self.read_tour(tour_fname) | |
| # remove dump (?) files | |
| try: | |
| os.remove(instance_fname); os.remove(tour_fname) | |
| except OSError as e: | |
| pass | |
| fname_list = glob.glob("*.sol") | |
| fname_list.extend(glob.glob("*.res")) | |
| for fname in fname_list: | |
| try: | |
| os.remove(fname) | |
| except OSError as e: | |
| # do nothing | |
| pass | |
| # subprocess.run(f"rm {instance_name}.sol", shell=True) | |
| return tours | |
| def read_tour(self, tour_fname): | |
| """ | |
| Parameters | |
| ---------- | |
| tour_fname: str | |
| path to an output tour | |
| Returns | |
| ------- | |
| tour: 2d list [num_vehicles(1) x seq_length] | |
| """ | |
| if not os.path.exists(tour_fname): # fails to solve the instance | |
| return | |
| tour = [] | |
| with open(tour_fname, "r") as f: | |
| for i, line in enumerate(f): | |
| if i == 0: | |
| continue | |
| read_tour = line.split() | |
| tour.extend(read_tour) | |
| tour.append(tour[0]) | |
| return [list(map(int, tour))] | |
| def preprocess_data(self, node_feats): | |
| # convert float to integer approximately | |
| return { | |
| key: (node_feat * self.large_value).astype(np.int64) | |
| if key == "coords" else | |
| node_feat | |
| for key, node_feat in node_feats.items() | |
| } | |