```from hashindex import Index
from math import hypot
import anneal
import random

def sort_paths_greedy(paths, reversable=True):
first = max(paths, key=lambda x: x)
paths.remove(first)
result = [first]
points = []
for path in paths:
x1, y1 = path
x2, y2 = path[-1]
points.append((x1, y1, path, False))
if reversable:
points.append((x2, y2, path, True))
index = Index(points)
while index.size:
x, y, path, reverse = index.search(result[-1][-1])
x1, y1 = path
x2, y2 = path[-1]
index.remove((x1, y1, path, False))
if reversable:
index.remove((x2, y2, path, True))
if reverse:
result.append(list(reversed(path)))
else:
result.append(path)
return result

def sort_paths(paths, iterations=100000, reversable=True):
'''
This function re-orders a set of 2D paths (polylines) to minimize the
distance required to visit each path. This is useful for 2D plotting to
reduce wasted movements where the instrument is not drawing.

If allowed, the algorithm will also reverse some paths if doing so reduces
the total distance.

The code uses simulated annealing as its optimization algorithm. The number
of iterations can be increased to improve the chances of finding a perfect
solution. However, a perfect solution isn't necessarily required - we just
want to find something good enough.

With randomly generated paths, the algorithm can quickly find a solution
that reduces the extra distance to ~25 percent of its original value.
'''
state = Model(list(paths), reversable)
max_temp = anneal.get_max_temp(state, 10000)
min_temp = max_temp / 1000.0
state = anneal.anneal(state, max_temp, min_temp, iterations)
for path, reverse in zip(state.paths, state.reverse):
if reverse:
path.reverse()
return state.paths

def sort_points(points, iterations=100000):
'''
Like sort_paths, but operates on individual points instead.
This is basically a traveling salesman optimization.
'''
paths = [[x] for x in points]
paths = sort_paths(paths, iterations, False)
points = [x for x in paths]
return points

class Model(object):
def __init__(self, paths, reversable=True, reverse=None, distances=None, total_distance=None):
self.paths = paths
self.reversable = reversable
self.reverse = reverse or [False] * len(self.paths)
if distances:
self.total_distance = total_distance or 0
self.distances = distances
else:
self.total_distance = 0
self.distances =  * (len(paths) - 1)
def subtract_distances(self, indexes):
n = len(self.distances)
for i in indexes:
if i >= 0 and i < n:
self.total_distance -= self.distances[i]
n = len(self.distances)
for i in indexes:
if i < 0 or i >= n:
continue
j = i + 1
if self.reverse[i]:
x1, y1 = self.paths[i]
else:
x1, y1 = self.paths[i][-1]
if self.reverse[j]:
x2, y2 = self.paths[j][-1]
else:
x2, y2 = self.paths[j]
self.distances[i] = hypot(x2 - x1, y2 - y1)
self.total_distance += self.distances[i]
def energy(self):
# return the total extra distance for this ordering
return self.total_distance
def do_move(self):
if self.reversable and random.random() < 0.25:
# mutate by reversing a random path
n = len(self.paths) - 1
i = random.randint(0, n)
indexes = [i - 1, i]
self.subtract_distances(indexes)
self.reverse[i] = not self.reverse[i]
return (1, i, 0)
else:
# mutate by swapping two random paths
n = len(self.paths) - 1
i = random.randint(0, n)
j = random.randint(0, n)
indexes = set([i - 1, i, j - 1, j])
self.subtract_distances(indexes)
self.paths[i], self.paths[j] = self.paths[j], self.paths[i]
return (0, i, j)
def undo_move(self, undo):
# undo the previous mutation
mode, i, j = undo
if mode == 0:
indexes = set([i - 1, i, j - 1, j])
self.subtract_distances(indexes)
self.paths[i], self.paths[j] = self.paths[j], self.paths[i]
else:
indexes = [i - 1, i]
self.subtract_distances(indexes)
self.reverse[i] = not self.reverse[i]
def copy(self):
# make a copy of the model
return Model(
list(self.paths), self.reversable, list(self.reverse),
list(self.distances), self.total_distance)

def test(n_paths, n_iterations, seed=None):
random.seed(seed)
paths = []
for _ in range(n_paths):
x1 = random.random()
y1 = random.random()
x2 = random.random()
y2 = random.random()
path = [(x1, y1), (x2, y2)]
paths.append(path)
before = Model(paths).energy()
if n_iterations:
paths = sort_paths(paths, n_iterations)
else:
paths = sort_paths_greedy(paths)
after = Model(paths).energy()
pct = 100.0 * after / before
return pct

if __name__ == '__main__':
# test the module
for n_paths in [10, 100, 1000, 10000]:
for n_iterations in [None, 10, 100, 1000, 10000, 100000, 1000000]:
pct = test(n_paths, n_iterations, 123)
print n_paths, n_iterations, pct
```