```from time import time
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rcParams
import math
from scipy.spatial.distance import pdist, squareform
from sklearn.decomposition import PCA
import os

from tsptw_with_ortools import Solver
from config import get_config, print_config

# Compute a sequence's reward
def reward(tsptw_sequence,speed):
# Convert sequence to tour (end=start)
tour = np.concatenate((tsptw_sequence,np.expand_dims(tsptw_sequence[0],0)))
# Compute tour length
inter_city_distances = np.sqrt(np.sum(np.square(tour[:-1,:2]-tour[1:,:2]),axis=1))
distance = np.sum(inter_city_distances)
# Compute develiry times at each city and count late cities
elapsed_time = -10
late_cities = 0
for i in range(tsptw_sequence.shape[0]-1):
travel_time = inter_city_distances[i]/speed
tw_open = tour[i+1,2]
tw_close = tour[i+1,3]
elapsed_time += travel_time
if elapsed_time <= tw_open:
elapsed_time = tw_open
elif elapsed_time > tw_close:
late_cities += 1
# Reward
return distance + 100000000*late_cities

# Swap city[i] with city[j] in sequence
def swap2opt(tsptw_sequence,i,j):
new_tsptw_sequence = np.copy(tsptw_sequence)
new_tsptw_sequence[i:j+1] = np.flip(tsptw_sequence[i:j+1], axis=0) # flip or swap ?
return new_tsptw_sequence

# One step of 2opt  = one double loop and return first improved sequence
def step2opt(tsptw_sequence,speed):
seq_length = tsptw_sequence.shape[0]
distance = reward(tsptw_sequence,speed)
for i in range(1,seq_length-1):
for j in range(i+1,seq_length):
new_tsptw_sequence = swap2opt(tsptw_sequence,i,j)
new_distance = reward(new_tsptw_sequence,speed)
if new_distance < distance:
return new_tsptw_sequence
return tsptw_sequence

class DataGenerator(object):

# Initialize a DataGenerator
def __init__(self,config):
self.batch_size = config.batch_size
self.dimension = config.input_dimension
self.max_length = config.max_length
self.speed = config.speed
self.kNN = config.kNN # int for random k_nearest_neighbor
self.width = config.width_mean, config.width_std # time window width gaussian distribution [mean,std]
self.pretrain = config.pretrain
# Create Solver and Data Generator
self.solver = Solver(self.max_length, self.speed) # reference solver for TSP-TW (Google_OR_tools)

# Solve an instance with reference solver
def solve_instance(self, sequence, tw_open, tw_close):
# Calculate distance matrix
precision = 1 #20 #int(self.speed)
dist_array = pdist(sequence)
dist_matrix = squareform(dist_array)
# Call OR Tools to solve instance
demands = np.zeros(tw_open.size)
tour, tour_length, delivery_time = self.solver.run(precision*dist_matrix, demands, precision*(1+tw_open), precision*(1+tw_close)) # a tour is a permutation + start_index    # Rq: +1 for depot offset
tour_length= tour_length/precision
delivery_time = np.asarray(delivery_time)/precision - 1 # offset -1 because depot opens at -1

# Iterate step2opt max_iter times
def loop2opt(self, tsptw_sequence, max_iter=2000, speed=1.):
best_reward = reward(tsptw_sequence,speed)
new_tsptw_sequence = np.copy(tsptw_sequence)
for _ in range(max_iter):
new_tsptw_sequence = step2opt(new_tsptw_sequence,speed)
new_reward = reward(new_tsptw_sequence,speed)
if new_reward < best_reward:
best_reward = new_reward
else:
break
return new_tsptw_sequence, best_reward

def get_tour_length(self, sequence):
# Convert sequence to tour (end=start)
tour = np.concatenate((sequence,np.expand_dims(sequence[0],0)))
# Compute tour length
inter_city_distances = np.sqrt(np.sum(np.square(tour[:-1]-tour[1:]),axis=1))
return np.sum(inter_city_distances)

# Reorder sequence with random k NN (TODO: Less computations)
def k_nearest_neighbor(self, sequence):
# Calculate dist_matrix
dist_array = pdist(sequence)
dist_matrix = squareform(dist_array)
# Construct tour
new_sequence = [sequence[0]]
current_city = 0
visited_cities = [0]
for i in range(1,len(sequence)):
j = np.random.randint(0,min(len(sequence)-i,self.kNN))
next_city = [index for index in dist_matrix[current_city].argsort() if index not in visited_cities][j]
visited_cities.append(next_city)
new_sequence.append(sequence[next_city])
current_city = next_city
return np.asarray(new_sequence)

# Generate random TSP-TW instance
def gen_instance(self, test_mode=True, seed=0):
if seed!=0: np.random.seed(seed)
# Randomly generate (max_length+1) city integer coordinates in [0,100[      # Rq: +1 for depot
sequence = np.random.randint(100, size=(self.max_length+1, self.dimension))
if self.pretrain == False:
sequence = self.k_nearest_neighbor(sequence) # k nearest neighbour tour (reverse order - depot end)
# Principal Component Analysis to center & rotate coordinates
pca = PCA(n_components=self.dimension)
sequence_ = pca.fit_transform(sequence)
# TW constraint 1 (open time)
if self.pretrain == True:
tw_open = np.random.randint(100, size=(self.max_length, 1)) # t_open random integer in [0,100[
tw_open = np.concatenate((tw_open,[[-1]]), axis=0) # depot opens at -1
tw_open[::-1].sort(axis=0) # sort cities by TW open constraint (reverse order)
else: # Open time defined by kNN tour
ordered_seq = sequence[::-1]
inter_city_distances = np.sqrt(np.sum(np.square(ordered_seq[1:]-ordered_seq[:-1]),axis=1))
time_at_cities = np.cumsum(inter_city_distances/self.speed,axis=0)
time_at_cities = np.expand_dims(np.floor(time_at_cities).astype(int),axis=1)
tw_open = np.concatenate(([[-1]],time_at_cities), axis=0) # depot opens at -1
tw_open = tw_open[::-1] # TW open constraint sorted (reverse order)   Rq: depot = tw_open[-1], tw_width[-1] and sequence[-1]
# TW constraint 2 (time width): Gaussian or uniform distribution
tw_width = np.abs(np.random.normal(loc=self.width[0], scale=self.width[1], size=(self.max_length, 1)))  # gaussian distribution
tw_width = np.concatenate((tw_width,[[1]]), axis=0) # depot opened for 1
tw_width = np.ceil(tw_width).astype(int)
tw_close = tw_open+tw_width
# TW feature 1 = Centered mean time (invariance)
tw_mean_ = (tw_open+tw_close)/2
tw_mean_ -= np.mean(tw_mean_)
# TW feature 2 = Width
tw_width_ = tw_width
print(tw_width_)
# Concatenate input (sorted by time) and scale to [0,1[
input_ = np.concatenate((sequence_,tw_mean_,tw_width_), axis=1)/100
if test_mode == True:
return input_, sequence, tw_open, tw_close
else:
return input_

# Generate random batch for training procedure
def train_batch(self):
input_batch = []
for _ in range(self.batch_size):
# Generate random TSP-TW instance
input_ = self.gen_instance(test_mode=False)
# Store batch
input_batch.append(input_)
return input_batch

# Generate random batch for testing procedure
def test_batch(self, seed=0):
# Generate random TSP-TW instance
input_, or_sequence, tw_open, tw_close = self.gen_instance(test_mode=True, seed=seed)
# Store batch
input_batch = np.tile(input_,(self.batch_size,1,1))
return input_batch, or_sequence, tw_open, tw_close

# Plot a tour
def visualize_2D_trip(self,trip,tw_open,tw_close):
plt.figure(figsize=(30,30))
rcParams.update({'font.size': 22})
# Plot cities
colors = ['red'] # Depot is first city
for i in range(len(tw_open)-1):
colors.append('blue')
plt.scatter(trip[:,0], trip[:,1], color=colors, s=200)
# Plot tour
tour=np.array(list(range(len(trip))) + [0])
X = trip[tour, 0]
Y = trip[tour, 1]
plt.plot(X, Y,"--", markersize=100)
# Annotate cities with TW
tw_open = np.rint(tw_open)
tw_close = np.rint(tw_close)
time_window = np.concatenate((tw_open,tw_close),axis=1)
for tw, (x, y) in zip(time_window,(zip(X,Y))):
plt.annotate(tw,xy=(x, y))
plt.xlim(0,60)
plt.ylim(0,60)
plt.show()

# Heatmap of permutations (x=cities; y=steps)
def visualize_sampling(self,permutations):
max_length = len(permutations[0])
grid = np.zeros([max_length,max_length]) # initialize heatmap grid to 0
transposed_permutations = np.transpose(permutations)
for t, cities_t in enumerate(transposed_permutations): # step t, cities chosen at step t
city_indices, counts = np.unique(cities_t,return_counts=True,axis=0)
for u,v in zip(city_indices, counts):
grid[t][u]+=v # update grid with counts from the batch of permutations
# plot heatmap
fig = plt.figure()
rcParams.update({'font.size': 22})
ax.set_aspect('equal')
plt.imshow(grid, interpolation='nearest', cmap='gray')
plt.colorbar()
plt.title('Sampled permutations')
plt.ylabel('Time t')
plt.xlabel('City i')
plt.show()

# Heatmap of attention (x=cities; y=steps)
def visualize_attention(self,attention):
# plot heatmap
fig = plt.figure()
rcParams.update({'font.size': 22})
ax.set_aspect('equal')
plt.imshow(attention, interpolation='nearest', cmap='hot')
plt.colorbar()
plt.title('Attention distribution')
plt.ylabel('Step t')
plt.xlabel('Attention_t')
plt.show()

dataset = {}
shrinkage = 80
for file_name in os.listdir('benchmark/'+dir_):
if 'solution' in file_name: continue
# Gather data
data = open('benchmark/'+dir_+'/'+file_name, 'r')
x,y,t_open,t_close = [],[],[],[]
for i,line in enumerate(data):
if i>5:
line = line.split()
if line[0]!='999':
x.append(int(float(line[1])))
y.append(int(float(line[2])))
t_open.append(int(float(line[4])))
t_close.append(int(float(line[5])))
# TW constraint 1 (open time)
t_open = np.asarray(t_open) # open time
sorted_index = np.argsort(t_open)[::-1] # sort cities by TW open constraint (reverse order)
tw_open = t_open[sorted_index]
tw_open = np.expand_dims(tw_open,axis=1)
tw_open_ = shrinkage*tw_open/tw_open[0]-1  # scale open time in [0,100[ (depot opens at -1)
# TW constraint 2 (close time) ############################### RESCALE ??
t_close = np.asarray(t_close)-1 ############################### depot ?
tw_close = t_close[sorted_index]
tw_close = np.expand_dims(tw_close,axis=1)
tw_close_ = shrinkage*tw_close/tw_open[0]-1 # scale close time
tw_close_[-1] = 0 # depot open till 0
# Coordinates
seq = np.stack((x,y),axis=1) # city integer coordinates in [0,100[ ############################### RESCALE ??
sequence = seq[sorted_index]
pca = PCA(n_components=self.dimension) # Principal Component Analysis to center & rotate coordinates
sequence_ = pca.fit_transform(sequence)
sequence_ = self.speed*shrinkage*sequence_/tw_open[0] # scale sequence
# TW feature 1 = Centered mean time (invariance)
tw_mean_ = (tw_open_+tw_close_)/2
tw_mean_ -= np.mean(tw_mean_)
# TW feature 2 = Width
tw_width_ = tw_close_-tw_open_
# Concatenate input (sorted by time) and scale to [0,1[
input_ = np.concatenate((sequence_,tw_mean_,tw_width_), axis=1)/100
# Gather solution
solution = open('benchmark/'+dir_+'/'+file_name+'.solution', 'r')
for i,line in enumerate(solution):
if i==0: opt_permutation = np.asarray(line.split()).astype(int)-1
if i==1: opt_length = int(line.split()[0])
opt_length = self.get_tour_length(seq[opt_permutation])/100
# Save data
dataset[file_name]={'input_': input_, 'sequence':sequence, 'tw_open':tw_open, 'tw_close':tw_close, 'optimal_sequence':seq[opt_permutation],
'optimal_tw_open':np.expand_dims(t_open[opt_permutation],axis=1), 'optimal_tw_close':np.expand_dims(t_close[opt_permutation],axis=1), 'optimal_length':opt_length}

return dataset

if __name__ == "__main__":

# Config
config, _ = get_config()
dataset = DataGenerator(config)

# Generate some data
#input_batch = dataset.train_batch()
input_batch, or_sequence, tw_open, tw_close = dataset.test_batch(seed=0)
print()

# Some print
#print('Input batch: \n',100*input_batch)
#print(np.rint(np.mean(100*input_batch,1)))

# 2D plot for coord batch
#dataset.visualize_2D_trip(or_sequence[::-1], tw_open[::-1], tw_close[::-1])

# Solve to optimality and plot solution
#or_permutation, or_tour_length, or_delivery_time = dataset.solve_instance(or_sequence, tw_open, tw_close)
#print('Solver tour length: \n', or_tour_length/100)
#print('Time var: \n', or_delivery_time)
#dataset.visualize_2D_trip(or_sequence[or_permutation], tw_open[or_permutation], tw_close[or_permutation])
#dataset.visualize_sampling([or_permutation])