I have tested, and my first instinct is that the gen_subtour()
is not working well, maybe it is changing the route with too many steps.
I would try different versions and check how the effect it. The SA scheme seem to be working well, it is the proposals where I think the error is.
Anyway, here some code that hopefully will help you test better.
I use the pdist
to pre-calculate manhatten distance, i.e;
import numpy as np
from scipy.spatial.distance import pdist, cdist, squareform
coords = np.array([[375, 375],[161, 190], [186, 169],[185, 124],
[122, 104],[109, 258], [55, 153] ,[120, 49],
[39, 85] ,[59, 250] , [17, 310] ,[179, 265],
[184, 198]])
Y = pdist(coords, 'cityblock')
distance_matrix = squareform(Y)
nodes_count = coords.shape[0]
And define start with
def random_start():
"""
Random start, returns a state
"""
a = np.arange(0,nodes_count)
np.random.shuffle(a)
return a
The objective function, where we have the version that we don't return to the origin;
def objective_function( route ):
# uncomment when testing new/modify neighbors
# assert check_all_nodes_visited(route)
return np.sum( distance_matrix[route[1:],route[:-1]] )
I have here 3 kinds of proposals, based on a route;
def random_swap( route ):
"""
Random Swap - a Naive neighbour function
Will only work for small instances of the problem
"""
route_copy = route.copy()
random_indici = np.random.choice( route , 2, replace = False)
route_copy[ random_indici[0] ] = route[ random_indici[1] ]
route_copy[ random_indici[1] ] = route[ random_indici[0] ]
return route_copy
def vertex_insert( route, nodes=1 ):
"""
Vertex Insert Neighbour, inspired by
http://www.sciencedirect.com/science/article/pii/S1568494611000573
"""
route_copy = route.copy()
random_indici = np.random.choice( route , 2, replace = False)
index_of_point_to_reroute = random_indici[0]
value_of_point_to_reroute = route[ random_indici[0] ]
index_of_new_place = random_indici[1]
route_copy = np.delete(route_copy, index_of_point_to_reroute)
route_copy = np.insert(route_copy, index_of_new_place, values=value_of_point_to_reroute)
return route_copy
def block_reverse( route, nodes=1 ):
"""
Block Reverse Neighbour, inspired by
http://www.sciencedirect.com/science/article/pii/S1568494611000573
Note that this is a random 2-opt operation.
"""
route_copy = route.copy()
random_indici = np.random.choice( route , 2, replace = False)
index_of_cut_left = np.min(random_indici)
index_of_cut_right = np.max(random_indici)
route_copy[ index_of_cut_left:index_of_cut_right ] = np.flip(route_copy[ index_of_cut_left:index_of_cut_right ])
return route_copy
Optionally, you can to a 2-opt round after the SA, to make sure there are no crossing.
def swap_for_2opt( route, i, k):
"""
Helper for 2-opt search
"""
route_copy = route.copy()
index_of_cut_left = i
index_of_cut_right = k
route_copy[ index_of_cut_left:index_of_cut_right ] = np.flip(route_copy[ index_of_cut_left:index_of_cut_right ])
return route_copy
def local_search_2opt( route ):
"""
Local Optimum with 2-opt
https://en.wikipedia.org/wiki/2-opt
"""
steps_since_improved = 0
still_improving = True
route = route.copy()
while still_improving :
for i in range( route.size - 1 ):
for k in np.arange( i + 1, route.size ):
alt_route = swap_for_2opt(route, i, k)
if objective_function(alt_route) < objective_function(route):
route = alt_route.copy()
steps_since_improved = 0
steps_since_improved += 1
if steps_since_improved > route.size + 1:
still_improving = False
break
return route
And use frigidum for SA
import frigidum
local_opt = frigidum.sa(random_start=random_start,
objective_function=objective_function,
neighbours=[random_swap, vertex_insert, block_reverse],
copy_state=frigidum.annealing.naked,
T_start=10**5,
alpha=.95,
T_stop=0.001,
repeats=10**2,
post_annealing = local_search_2opt)
The returned route is almost always 1145.
I have posted general hints and tips on frigidum homepage.