Skip to content

Commit bc94c19

Browse files
authored
Merge pull request #8 from soukupak/add_sorting
Add entity sorting for less travel
2 parents d6625bc + d25c7a9 commit bc94c19

File tree

7 files changed

+277
-5
lines changed

7 files changed

+277
-5
lines changed

src/py2opt/__init__.py

Whitespace-only changes.

src/py2opt/routefinder.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import random
2+
import time
3+
4+
from py2opt.solver import Solver
5+
6+
7+
class RouteFinder:
8+
def __init__(self, distance_matrix, cities_names, iterations=5, writer_flag=False, method='py2opt', return_to_begin=False, verbose=True):
9+
self.distance_matrix = distance_matrix
10+
self.iterations = iterations
11+
self.return_to_begin = return_to_begin
12+
self.writer_flag = writer_flag
13+
self.cities_names = cities_names
14+
self.verbose = verbose
15+
16+
def solve(self):
17+
start_time = round(time.time() * 1000)
18+
elapsed_time = 0
19+
iteration = 0
20+
best_distance = 0
21+
best_route = []
22+
best_distances = []
23+
24+
while iteration < self.iterations:
25+
num_cities = len(self.distance_matrix)
26+
if self.verbose:
27+
print(round(elapsed_time), 'msec')
28+
initial_route = [0] + random.sample(range(1, num_cities), num_cities - 1)
29+
if self.return_to_begin:
30+
initial_route.append(0)
31+
tsp = Solver(self.distance_matrix, initial_route)
32+
new_route, new_distance, distances = tsp.two_opt()
33+
34+
if iteration == 0:
35+
best_distance = new_distance
36+
best_route = new_route
37+
else:
38+
pass
39+
40+
if new_distance < best_distance:
41+
best_distance = new_distance
42+
best_route = new_route
43+
best_distances = distances
44+
45+
elapsed_time = round(time.time() * 1000) - start_time
46+
iteration += 1
47+
48+
if self.writer_flag:
49+
self.writer(best_route, best_distance, self.cities_names)
50+
51+
if self.cities_names:
52+
best_route = [self.cities_names[i] for i in best_route]
53+
return best_distance, best_route
54+
else:
55+
return best_distance, best_route
56+
57+
@staticmethod
58+
def writer(best_route, best_distance, cities_names):
59+
f = open("../results.txt", "w+")
60+
for i in best_route:
61+
f.write(cities_names[i])
62+
f.write("\n")
63+
print(cities_names[i])
64+
f.write(str(best_distance))
65+
f.close()

src/py2opt/solver.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import itertools
2+
import numpy as np
3+
4+
5+
class Solver:
6+
def __init__(self, distance_matrix, initial_route):
7+
self.distance_matrix = distance_matrix
8+
self.num_cities = len(self.distance_matrix)
9+
self.initial_route = initial_route
10+
self.best_route = []
11+
self.best_distance = 0
12+
self.distances = []
13+
14+
def update(self, new_route, new_distance):
15+
self.best_distance = new_distance
16+
self.best_route = new_route
17+
return self.best_distance, self.best_route
18+
19+
def exhaustive_search(self):
20+
self.best_route = [0] + list(range(1, self.num_cities))
21+
self.best_distance = self.calculate_path_dist(self.distance_matrix, self.best_route)
22+
23+
for new_route in itertools.permutations(list(range(1, self.num_cities))):
24+
new_distance = self.calculate_path_dist(self.distance_matrix, [0] + list(new_route[:]))
25+
26+
if new_distance < self.best_distance:
27+
self.update([0] + list(new_route[:]), new_distance)
28+
self.distances.append(self.best_distance)
29+
30+
return self.best_route, self.best_distance, self.distances
31+
32+
def two_opt(self, improvement_threshold=0.01):
33+
self.best_route = self.initial_route
34+
self.best_distance = self.calculate_path_dist(self.distance_matrix, self.best_route)
35+
improvement_factor = 1
36+
37+
while improvement_factor > improvement_threshold:
38+
previous_best = self.best_distance
39+
for swap_first in range(1, self.num_cities - 2):
40+
for swap_last in range(swap_first + 1, self.num_cities - 1):
41+
before_start = self.best_route[swap_first - 1]
42+
start = self.best_route[swap_first]
43+
end = self.best_route[swap_last]
44+
after_end = self.best_route[swap_last+1]
45+
before = self.distance_matrix[before_start][start] + self.distance_matrix[end][after_end]
46+
after = self.distance_matrix[before_start][end] + self.distance_matrix[start][after_end]
47+
if after < before:
48+
new_route = self.swap(self.best_route, swap_first, swap_last)
49+
new_distance = self.calculate_path_dist(self.distance_matrix, new_route)
50+
self.update(new_route, new_distance)
51+
52+
improvement_factor = 1 - self.best_distance/previous_best
53+
return self.best_route, self.best_distance, self.distances
54+
55+
@staticmethod
56+
def calculate_path_dist(distance_matrix, path):
57+
"""
58+
This method calculates the total distance between the first city in the given path to the last city in the path.
59+
"""
60+
path_distance = 0
61+
for ind in range(len(path) - 1):
62+
path_distance += distance_matrix[path[ind]][path[ind + 1]]
63+
return float("{0:.2f}".format(path_distance))
64+
65+
@staticmethod
66+
def swap(path, swap_first, swap_last):
67+
path_updated = np.concatenate((path[0:swap_first],
68+
path[swap_last:-len(path) + swap_first - 1:-1],
69+
path[swap_last + 1:len(path)]))
70+
return path_updated.tolist()

src/py2opt/utils.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import numpy as np
2+
from math import sin, cos, sqrt, atan2, radians
3+
4+
5+
class GeographicalPositionTest:
6+
def __init__(self, file_name):
7+
self.file_name = file_name
8+
9+
def build_dist_matrix(self, cities_names, cities_coordinates):
10+
"""
11+
This function creates a matrix containing pair distance among all cities in kilometers. Distance between each
12+
city by itself is equal to zero.
13+
"""
14+
num_cities = len(cities_names)
15+
distance_matrix = np.zeros([num_cities, num_cities])
16+
for city_departure in cities_names:
17+
for city_arrival in cities_names:
18+
i = cities_names.index(city_departure)
19+
j = cities_names.index(city_arrival)
20+
co_i = cities_coordinates[city_departure]
21+
co_j = cities_coordinates[city_arrival]
22+
distance_matrix[i][j] = self.calculate_pair_dist(co_i, co_j)
23+
return distance_matrix
24+
25+
def open_file(self):
26+
city_names = []
27+
cities_coordinates = {}
28+
with open(self.file_name, "r") as file_stream:
29+
num_line = 0
30+
for line in file_stream:
31+
num_line += 1
32+
current_line = line.split(",")
33+
if self.is_valid(current_line):
34+
city_name = current_line[0]
35+
city_latitude = float(
36+
"{0:.2f}".format(float(current_line[1]) + float(current_line[2]) / 60))
37+
city_longitude = float(
38+
"{0:.2f}".format(float(current_line[3]) + float(current_line[4]) / 60))
39+
40+
cities_coordinates[city_name] = [city_latitude, city_longitude]
41+
city_names.append(city_name)
42+
else:
43+
print('This line', num_line, 'does not pass our test.')
44+
45+
num_cities = len(city_names)
46+
47+
return cities_coordinates, city_names, num_cities
48+
49+
@staticmethod
50+
def calculate_pair_dist(coordinates_1, coordinates_2):
51+
"""
52+
This function calculates distance between two cities in kilometers given geographical coordinates of two cities.
53+
"""
54+
latitude_first, longitude_first = coordinates_1
55+
latitude_second, longitude_second = coordinates_2
56+
57+
r = 6373.0
58+
a = (sin(radians(latitude_second - latitude_first) / 2)) ** 2 + \
59+
cos(radians(latitude_first)) * cos(radians(latitude_second)) * \
60+
(sin(radians(longitude_second - longitude_first) / 2)) ** 2
61+
c = 2 * atan2(sqrt(a), sqrt(1 - a))
62+
distance_kilometer = float("{0:.2f}".format(r * c))
63+
64+
return distance_kilometer
65+
66+
@staticmethod
67+
def is_valid(line):
68+
"""
69+
This boolean function check each line in the imported file to have exact 5 components. Moreover, it checks
70+
whether latitude and longitude degrees are between -180 and 180, and its corresponding minutes are between
71+
0 and 60.
72+
"""
73+
if len(line) == 5:
74+
latitude_degree = float(line[1])
75+
latitude_minute = float(line[2])
76+
longitude_degree = float(line[3])
77+
longitude_minute = float(line[4])
78+
79+
if -180 <= latitude_degree <= 180 and 0 <= latitude_minute <= 60 and \
80+
-180 <= longitude_degree <= 180 and 0 <= longitude_minute <= 60:
81+
return True
82+
else:
83+
return False

src/unicorn.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from math import *
2121
from unicorn.context import GCodeContext
2222
from unicorn.svg_parser import SvgParser
23+
from py2opt.routefinder import RouteFinder
2324

2425
class MyEffect(inkex.Effect):
2526
def __init__(self):
@@ -79,8 +80,21 @@ def effect(self):
7980
self.options.input_file)
8081
parser = SvgParser(self.document.getroot(), self.options.pause_on_layer_change)
8182
parser.parse()
82-
for entity in parser.entities:
83-
entity.get_gcode(self.context)
83+
84+
inkex.utils.debug(len(parser.entities))
85+
86+
87+
if len(parser.entities) > 1:
88+
distance_matrix = parser.get_distance_matrix()
89+
90+
route_finder = RouteFinder(distance_matrix, parser.entities, iterations=5)
91+
best_distance, best_route = route_finder.solve()
92+
93+
for entity in best_route:
94+
entity.get_gcode(self.context)
95+
else:
96+
for entity in parser.entities:
97+
entity.get_gcode(self.context)
8498

8599
MyEffect.save_raw(self, self.context.generate())
86100

src/unicorn/entities.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,33 @@
1-
from math import cos, sin, radians
1+
from math import cos, sin, radians, sqrt, pow
22
import pprint
33
import warnings
4+
import inkex
45

56
class Entity:
67
def __getattr__(self, name):
78
''' will only get called for undefined attributes '''
89
warnings.warn('No member "%s" contained in settings config.' % name)
910
return ''
11+
12+
def get_starting_point(self):
13+
return "NIE"
14+
15+
def distance(self, Entity):
16+
point_1 = self.get_starting_point()
17+
point_2 = Entity.get_starting_point()
18+
return sqrt(pow(point_2[0] - point_1[0], 2) + pow(point_2[1] - point_1[1], 2))
19+
1020
def get_gcode(self,context):
1121
#raise NotImplementedError()
1222
return "NIE"
1323

1424
class Line(Entity):
1525
def __str__(self):
1626
return "Line from [%.2f, %.2f] to [%.2f, %.2f]" % (self.start[0], self.start[1], self.end[0], self.end[1])
27+
28+
def get_starting_point(self):
29+
return self.start
30+
1731
def get_gcode(self,context):
1832
"Emit gcode for drawing line"
1933
context.codes.append("(" + str(self) + ")")
@@ -24,6 +38,10 @@ def get_gcode(self,context):
2438
class Circle(Entity):
2539
def __str__(self):
2640
return "Circle at [%.2f,%.2f], radius %.2f" % (self.center[0], self.center[1], self.radius)
41+
42+
def get_starting_point(self):
43+
return (self.center[0] - self.radius, self.center[1])
44+
2745
def get_gcode(self,context):
2846
"Emit gcode for drawing arc"
2947
start = (self.center[0] - self.radius, self.center[1])
@@ -39,6 +57,9 @@ def get_gcode(self,context):
3957
class Arc(Entity):
4058
def __str__(self):
4159
return "Arc at [%.2f, %.2f], radius %.2f, from %.2f to %.2f" % (self.center[0], self.center[1], self.radius, self.start_angle, self.end_angle)
60+
61+
def get_starting_point(self):
62+
return self.find_point(0)
4263

4364
def find_point(self,proportion):
4465
"Find point at the given proportion along the arc."
@@ -77,6 +98,12 @@ class PolyLine(Entity):
7798
def __str__(self):
7899
return "Polyline consisting of %d segments." % len(self.segments)
79100

101+
def get_starting_point(self):
102+
if hasattr(self, 'segments'):
103+
return self.segments[0][0]
104+
else:
105+
return None
106+
80107
def get_gcode(self,context):
81108
"Emit gcode for drawing polyline"
82109
if hasattr(self, 'segments'):

src/unicorn/svg_parser.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,10 +312,23 @@ def make_entity(self,node,mat):
312312
if node.tag == inkex.addNS(tag,ns) or node.tag == tag:
313313
constructor = SvgParser.entity_map[nodetype]
314314
entity = constructor()
315-
entity.load(node,mat)
316-
self.entities.append(entity)
315+
if not type(entity).__name__ == "SvgIgnoredEntity":
316+
entity.load(node,mat)
317+
self.entities.append(entity)
317318
return entity
318319
return None
320+
321+
def get_distance_matrix(self):
322+
ent_len = len(self.entities)
323+
distances = []
324+
325+
for i in range(ent_len):
326+
ent_i_distances = []
327+
for j in range(ent_len):
328+
ent_i_distances.append(self.entities[i].distance(self.entities[j]))
329+
distances.append(ent_i_distances)
330+
331+
return distances
319332

320333
def formatPath(a):
321334
"""Format SVG path data from an array"""

0 commit comments

Comments
 (0)