```import math
import random
import visualize_tsp
import matplotlib.pyplot as plt

class SimAnneal(object):
def __init__(self, coords, T=-1, alpha=-1, stopping_T=-1, stopping_iter=-1):
self.coords = coords
self.N = len(coords)
self.T = math.sqrt(self.N) if T == -1 else T
self.T_save = self.T  # save inital T to reset if batch annealing is used
self.alpha = 0.995 if alpha == -1 else alpha
self.stopping_temperature = 1e-8 if stopping_T == -1 else stopping_T
self.stopping_iter = 100000 if stopping_iter == -1 else stopping_iter
self.iteration = 1

self.nodes = [i for i in range(self.N)]

self.best_solution = None
self.best_fitness = float("Inf")
self.fitness_list = []

def initial_solution(self):
"""
Greedy algorithm to get an initial solution (closest-neighbour).
"""
cur_node = random.choice(self.nodes)  # start from a random node
solution = [cur_node]

free_nodes = set(self.nodes)
free_nodes.remove(cur_node)
while free_nodes:
next_node = min(free_nodes, key=lambda x: self.dist(cur_node, x))  # nearest neighbour
free_nodes.remove(next_node)
solution.append(next_node)
cur_node = next_node

cur_fit = self.fitness(solution)
if cur_fit < self.best_fitness:  # If best found so far, update best fitness
self.best_fitness = cur_fit
self.best_solution = solution
self.fitness_list.append(cur_fit)
return solution, cur_fit

def dist(self, node_0, node_1):
"""
Euclidean distance between two nodes.
"""
coord_0, coord_1 = self.coords[node_0], self.coords[node_1]
return math.sqrt((coord_0[0] - coord_1[0]) ** 2 + (coord_0[1] - coord_1[1]) ** 2)

def fitness(self, solution):
"""
Total distance of the current solution path.
"""
cur_fit = 0
for i in range(self.N):
cur_fit += self.dist(solution[i % self.N], solution[(i + 1) % self.N])
return cur_fit

def p_accept(self, candidate_fitness):
"""
Probability of accepting if the candidate is worse than current.
Depends on the current temperature and difference between candidate and current.
"""
return math.exp(-abs(candidate_fitness - self.cur_fitness) / self.T)

def accept(self, candidate):
"""
Accept with probability 1 if candidate is better than current.
Accept with probabilty p_accept(..) if candidate is worse.
"""
candidate_fitness = self.fitness(candidate)
if candidate_fitness < self.cur_fitness:
self.cur_fitness, self.cur_solution = candidate_fitness, candidate
if candidate_fitness < self.best_fitness:
self.best_fitness, self.best_solution = candidate_fitness, candidate
else:
if random.random() < self.p_accept(candidate_fitness):
self.cur_fitness, self.cur_solution = candidate_fitness, candidate

def anneal(self):
"""
Execute simulated annealing algorithm.
"""
# Initialize with the greedy solution.
self.cur_solution, self.cur_fitness = self.initial_solution()

print("Starting annealing.")
while self.T >= self.stopping_temperature and self.iteration < self.stopping_iter:
candidate = list(self.cur_solution)
l = random.randint(2, self.N - 1)
i = random.randint(0, self.N - l)
candidate[i : (i + l)] = reversed(candidate[i : (i + l)])
self.accept(candidate)
self.T *= self.alpha
self.iteration += 1

self.fitness_list.append(self.cur_fitness)

print("Best fitness obtained: ", self.best_fitness)
improvement = 100 * (self.fitness_list[0] - self.best_fitness) / (self.fitness_list[0])
print(f"Improvement over greedy heuristic: {improvement : .2f}%")

def batch_anneal(self, times=10):
"""
Execute simulated annealing algorithm `times` times, with random initial solutions.
"""
for i in range(1, times + 1):
print(f"Iteration {i}/{times} -------------------------------")
self.T = self.T_save
self.iteration = 1
self.cur_solution, self.cur_fitness = self.initial_solution()
self.anneal()

def visualize_routes(self):
"""
Visualize the TSP route with matplotlib.
"""
visualize_tsp.plotTSP([self.best_solution], self.coords)

def plot_learning(self):
"""
Plot the fitness through iterations.
"""
plt.plot([i for i in range(len(self.fitness_list))], self.fitness_list)
plt.ylabel("Fitness")
plt.xlabel("Iteration")
plt.show()
```