# ###################################################################
#
# Program: tsp_recuit_simule.py
# Author: Jean-Michel Richer
# Organisation: Computer Science Department,
# University of Angers,
# France
# Email: jean-michel.richer@univ-angers.fr
# Creation date: April, 2021
# Modification: April, 2021
#
# ###################################################################
#
# Aim:
#
# This program is an implementation of a Simulated Annealing
# algorithm used to find a solution to the travelling salesman
# problem.
#
# Objectif :
#
# Ce programme repose sur l'implantation d'un algorithme de
# Recuit Simulé afin de résoudre le problème du voyageur
# de commerce.
#
# ###################################################################
#
# License
#
# This program is a free software you can modifiy it and
# redistribute it for non profitable use.
#
# ###################################################################
#
# Sous cette forme l'algorithme peut parfois atteindre la solution
#
#[2, 3, 8, 0, 6, 5, 11, 7, 4, 9, 12, 1, 10], 18406
#=> 1 : [2, 3, 7, 0, 6, 5, 11, 8, 4, 9, 12, 1, 10], 13120
#=> 2 : [2, 3, 7, 0, 9, 5, 11, 8, 4, 6, 12, 1, 10], 10217
#=> 3 : [2, 3, 7, 0, 9, 5, 11, 8, 1, 6, 12, 4, 10], 8906
#=> 4 : [2, 3, 7, 0, 9, 5, 11, 1, 8, 6, 12, 4, 10], 8329
#=> 5 : [2, 3, 7, 0, 10, 5, 11, 1, 8, 6, 12, 4, 9], 7791
#=> 6 : [2, 9, 7, 0, 10, 5, 11, 1, 8, 6, 12, 4, 3], 7610
#=> 7 : [2, 0, 7, 9, 10, 5, 11, 1, 8, 6, 12, 4, 3], 7320
#=> 8 : [2, 7, 0, 9, 10, 5, 11, 1, 8, 6, 12, 4, 3], 7295
#=> 9 : [2, 7, 0, 9, 5, 10, 11, 1, 8, 6, 12, 4, 3], 7293
#[8, 6, 10, 7, 4, 1, 2, 9, 12, 3, 5, 11, 0], 17395
#=> 1 : [8, 6, 10, 7, 4, 0, 2, 9, 12, 3, 5, 11, 1], 13207
#=> 2 : [8, 6, 10, 4, 7, 0, 2, 9, 12, 3, 5, 11, 1], 11063
#=> 3 : [8, 6, 12, 4, 7, 0, 2, 9, 10, 3, 5, 11, 1], 8951
#=> 4 : [8, 6, 12, 4, 7, 0, 2, 9, 3, 10, 5, 11, 1], 8101
#=> 5 : [8, 6, 12, 4, 7, 0, 2, 3, 9, 10, 5, 11, 1], 7817
#=> 6 : [8, 6, 12, 4, 2, 0, 7, 3, 9, 10, 5, 11, 1], 7736
#=> 7 : [8, 6, 12, 4, 3, 0, 7, 2, 9, 10, 5, 11, 1], 7345
#=> 8 : [8, 6, 12, 4, 3, 2, 7, 0, 9, 10, 5, 11, 1], 7295
#=> 9 : [8, 6, 12, 4, 3, 2, 7, 0, 9, 5, 10, 11, 1], 7293
import random
import sys
import math
import numpy as np
import matplotlib.pyplot as plt
# ===================================================================
# Les données en entrée sont les suivantes :
# - une liste des villes sous forme de liste de chaines de caractères
# - les distances entre les villes sous forme d'une matrice d'entiers
# (en l'occurrence il s'agit d'une liste de liste d'entiers)
# ===================================================================
villes = [ "1. New York, NY",
"2. Los Angeles, CA",
"3. Chicago, IL",
"4. Minneapolis, MN",
"5. Denver, CO",
"6. Dallas, TX",
"7. Seattle, WA",
"8. Boston, MA",
"9. San Francisco, CA",
"10. St. Louis, MI",
"11. Houston, TX",
"12. Phoenix, AZ",
"13. Salt Lake City, UT" ]
distances = [
[0, 2451, 713, 1018, 1631, 1374, 2408, 213, 2571, 875, 1420, 2145, 1972],
[2451, 0, 1745, 1524, 831, 1240, 959, 2596, 403, 1589, 1374, 357, 579],
[713, 1745, 0, 355, 920, 803, 1737, 851, 1858, 262, 940, 1453, 1260],
[1018, 1524, 355, 0, 700, 862, 1395, 1123, 1584, 466, 1056, 1280, 987],
[1631, 831, 920, 700, 0, 663, 1021, 1769, 949, 796, 879, 586, 371],
[1374, 1240, 803, 862, 663, 0, 1681, 1551, 1765, 547, 225, 887, 999],
[2408, 959, 1737, 1395, 1021, 1681, 0, 2493, 678, 1724, 1891, 1114, 701],
[213, 2596, 851, 1123, 1769, 1551, 2493, 0, 2699, 1038, 1605, 2300, 2099],
[2571, 403, 1858, 1584, 949, 1765, 678, 2699, 0, 1744, 1645, 653, 600],
[875, 1589, 262, 466, 796, 547, 1724, 1038, 1744, 0, 679, 1272, 1162],
[1420, 1374, 940, 1056, 879, 225, 1891, 1605, 1645, 679, 0, 1017, 1200],
[2145, 357, 1453, 1280, 586, 887, 1114, 2300, 653, 1272, 1017, 0, 504],
[1972, 579, 1260, 987, 371, 999, 701, 2099, 600, 1162, 1200, 504, 0]
]
# ===================================================================
# On définit un chemin (configuration) comme la liste des villes
# parcourues et le coût qui lui est associé
# ===================================================================
class Path(object):
"""
a Path is the list of cities visited and the distance travelled
"""
def __init__(self, chemin, distance):
self.chemin = chemin
self.distance = distance
def __str__(self):
return str(self.chemin) + ", " + str(self.distance)
def copy(self):
return type(self)(self.chemin.copy(), self.distance)
# ===================================================================
# On définit une class TSP pour stocker et manipuler les données
# et résoudre le problème
#
# ===================================================================
class TSP(object):
"""
Class used to solve the problem that implements a Local Search
algorithm
"""
# -----------------------------------------------------
# Le constructeur modifie les données de manière à
# utiliser des indices qui commencent à 1 et non à 0
#
# On modifie la matrice des distances en ajoutant une
# ligne et une colonne de zéros, puis on convertit tous
# les 0 en une distance égale à la valeur maximum trouvée
# dans la matrice + 1, ce qui permettra de rechercher une
# distance minimum sans obtenir de zéro
# -----------------------------------------------------
def __init__( self, villes, distances ):
"""
Constructor
Parameters
----------
villes : list of string
list of cities names
distances : list of list of int
matrix of distances between cities
"""
self.nbr_villes = len(villes)
self.villes = villes.copy()
self.villes.insert(0, "-------")
self.distances = np.array( distances )
# add column of 0
@@@...@@@
@@@...@@@
# add line of 0
@@@...@@@
@@@...@@@
# find maximum distance
maximum = @@@...@@@
# replace all 0 distances by maximum + 1
@@@...@@@
print("---- Matrice des distances modifiée ----")
print(self.distances)
self.verbose = True
# -----------------------------------------------------
# Définition de la fonction objectif
# -----------------------------------------------------
def f_objectif( self, chemin ):
distance_parcourue = 0
@@@...@@@
return distance_parcourue
# -----------------------------------------------------
# Génération de voisins en échangeant des villes deux
# à deux. Seules les configurations voisines amélio-
# rantes sont gardées puis triées en ordre croissant.
# En fonction du nombre d'itérations on élimine
# certaines configurations :
# - si le nombre d'itérations est inférieu à 1000, on
# ne garde que les dix premières configurations
# améliorantes
# - au dela de 5000 itérations on ne garde que la
# première configuration améliorante
# -----------------------------------------------------
def voisinage( self, configuration, iteration ):
# liste des voisins améliorants
l = [ ]
distance_a_ameliorer = int(configuration.distance * 1.1)
for i in range( 1, self.nbr_villes-2):
for k in range( i+1, self.nbr_villes):
if i > self.nbr_villes or k > self.nbr_villes:
continue
@@@...@@@
if nouvelle_configuration.distance < distance_a_ameliorer:
l.append( nouvelle_configuration )
# tri des configuration en ordre croissant de la
# distance parcourue
@@@...@@@
return l
# -----------------------------------------------------
# Méthode de résolution basée sur le recuit simulé
# -----------------------------------------------------
def resolution_recuit_simule(self, t_i = 100.0, t_f = 0.001, alpha = 0.99):
self.liste_evolution_courante = []
self.liste_evolution_meilleur = []
#
# Génération de la configuration initiale
#
villes_dans_le_desordre = [ x for x in range(1, self.nbr_villes+1) ]
random.shuffle( villes_dans_le_desordre )
configuration_courante = Path( villes_dans_le_desordre, self. f_objectif( villes_dans_le_desordre ) )
print("Configuration initiale=", configuration_courante)
iteration = 1
#
# Paramètres de l'algorithme
#
# température initiale
T_initiale = t_i
# température finale
T_finale = t_f
# facteur de refroidissement (alpha)
facteur_de_refroidissement = alpha
T = T_initiale
meilleure_configuration = configuration_courante.copy()
# Tant qu'on a pas atteint la température finale
while T > T_finale:
str_T = "{:.4f}".format(T)
str_it = str(iteration)
str_cc = str( configuration_courante.distance )
str_mc = str( meilleure_configuration.distance)
print("T=" + str_T + ",iter=" + str_it + " => " + str_cc + " ! " + str_mc, end='')
# rechercher des voisins améliorants
l_voisins_ameliorants = self.voisinage( configuration_courante, iteration )
# si il n'y a qu'un seul voisin améliorant on le choisit
# sinon on choisit au hasard
if len(l_voisins_ameliorants) == 1:
i = 0
else:
i = random.randint(0, len(l_voisins_ameliorants)-1 )
configuration_voisine = l_voisins_ameliorants[ i ]
str_cv = str(configuration_voisine.distance)
str_lp = str(l_voisins_ameliorants[ 0 ].distance)
str_ld = str(l_voisins_ameliorants[ len(l_voisins_ameliorants)-1 ].distance)
print(",voisin=" + str_cv + " (" + str_lp + ", " + str_ld + ")", end='')
delta_f = configuration_voisine.distance - configuration_courante.distance
str_u = ""
str_exp = ""
# si le voisin est meilleur que la configuration courante
# on le garde
# sinon
# on prend un voisin qui détériore la solution
# avec une certaine probabilité qui diminue à
# mesure que la température diminue
if delta_f < 0.0:
configuration_courante = configuration_voisine
else:
# générer une valeur entre 0 et 1.0
u = @@@...@@@
# calculer exp(-delta_f / T)
e = @@@...@@@
str_u = "{:.4f}".format(u)
str_exp = "{:.4f}".format(e)
if (e > u):
configuration_courante = configuration_voisine
str_delta_f = "{:.4f}".format(delta_f)
print(",delta_f="+str_delta_f+",u="+str_u+",exp="+str_exp)
if configuration_courante.distance < meilleure_configuration.distance:
meilleure_configuration = configuration_courante.copy()
# diminution de la température
T = facteur_de_refroidissement * T
iteration += 1
self.liste_evolution_courante.append( configuration_courante.distance )
self.liste_evolution_meilleur.append( meilleure_configuration.distance )
return meilleure_configuration
# -----------------------------------------------------
# Affichage du chemin ainsi que des distances
# parcourues et de la distance totale
# -----------------------------------------------------
def print(self, chemin):
print()
print("chemin=",chemin)
print()
print("------------------------------------------")
print(" ville | dist. | totale")
print("------------------------------------------")
distance_totale = 0
print( "{0:25s} | {1:5d} | {2:5d}".format(self.villes[ chemin[0] ], 0, 0))
for ville in range(1, len(chemin)):
distance = self.distances[ chemin[ville-1] ][ chemin[ville] ]
distance_totale += distance
print( "{0:25s} | {1:5d} | {2:5d}".format(self.villes[ chemin[ville] ], distance, distance_totale))
distance = self.distances[ chemin[ville] ][ chemin[0] ]
distance_totale += distance
print( "{0:25s} | {1:5d} | {2:5d}".format(self.villes[ chemin[0] ], distance, distance_totale))
print()
# -----------------------------------------------------
# Affiche un graphique de l'évolution des scores
# (distances) de la configuration courante et de la
# meilleure configuration
# -----------------------------------------------------
def graphique(self):
x = range( len( self.liste_evolution_courante ) )
plt.grid()
plt.plot( x, self.liste_evolution_courante, label='configuration courante' )
plt.plot( x, self.liste_evolution_meilleur, label='meilleure configuration' )
plt.xlabel('itérations')
plt.legend()
plt.title('Evolution du Recuit Simulé')
plt.show()
# ===================================================================
# Programme principal
# ===================================================================
HELP_MESSAGE = """
Program arguments are:
-h or --help
to get this message
-q or --quiet
to remove verbose mode and avoid to display graphics
-i float or --t_init=float
initial temperature (default is 100.0)
-f float or --t_final=float
final temperature (default is 0.001)
-a float or --alpha=float
cooling factor (default is 0.99)
"""
# creer une instance du problème
tsp = TSP( villes, distances )
# température initiale
t_i = 100.0
# température finale
t_f = 0.001
#facteur_de_refroidissement
alpha = 0.99
#
# gestion des arguments du programme
#
if len(sys.argv) >= 1:
try:
i = 1
while i < len(sys.argv):
arg = sys.argv[i]
print(i, arg)
if arg == "-h" or arg == "--help":
print( HELP_MESSAGE )
elif arg == "-q" or arg == "--quiet":
tsp.verbose = False
elif arg == "-i":
i += 1
t_i = float(sys.argv[i])
elif arg.startswith("--t_init="):
t_i = float(arg.split('=')[1])
elif arg == "-f":
i += 1
t_f = float(sys.argv[i])
elif arg.startswith("--t_final="):
t_f = float(arg.split('=')[1])
elif arg == "-a":
i += 1
alpha = float(sys.argv[i])
elif arg.startswith("--alpha="):
alpha = float(arg.split('=')[1])
else:
raise ValueError("Unexpected argument")
i += 1
except:
print("="*40)
print(sys.exc_info()[1])
print("="*40)
print( HELP_MESSAGE )
sys.exit(1)
#
# recherche de la meilleure solution
#
configuration_finale = tsp.resolution_recuit_simule(t_i, t_f, alpha)
tsp.print(configuration_finale.chemin)
tsp.graphique()