Spaces:
Running
Running
sadFaceEmoji
commited on
Commit
•
ef35e05
1
Parent(s):
490218f
Add file
Browse files- pub_crawl_script.py +172 -0
pub_crawl_script.py
ADDED
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import osmnx as ox
|
2 |
+
import pandas as pd
|
3 |
+
import networkx as nx
|
4 |
+
import matplotlib.pyplot as plt
|
5 |
+
from ortools.constraint_solver import pywrapcp
|
6 |
+
from ortools.constraint_solver import routing_enums_pb2
|
7 |
+
|
8 |
+
class pub_crawl:
|
9 |
+
def __init__(self, df, G):
|
10 |
+
'''Initialise a pub_crawl instance
|
11 |
+
|
12 |
+
df: pd.Dataframe containing pubs 'name' and coordinates ('latitude', 'longitude')
|
13 |
+
G: nx.MultiDiGraph of the map area
|
14 |
+
'''
|
15 |
+
self.df = df
|
16 |
+
self.G = G
|
17 |
+
|
18 |
+
self.pub_names = df['name'].to_list()
|
19 |
+
self.initial_route = self.pub_names
|
20 |
+
|
21 |
+
self.optimal_route = None
|
22 |
+
self.optimal_distance = None
|
23 |
+
|
24 |
+
self.pub_nodes = self.create_pub_nodes()
|
25 |
+
|
26 |
+
def create_pub_nodes(self):
|
27 |
+
# Dictionary of pub names and coordinates
|
28 |
+
pubs_dict = self.df.drop('address', axis=1).set_index('name').T.to_dict('list')
|
29 |
+
|
30 |
+
# Get graph nodes for each pub
|
31 |
+
pub_nodes = {}
|
32 |
+
for k, v in pubs_dict.items():
|
33 |
+
pub_nodes[k] = ox.nearest_nodes(self.G, X = v[1], Y = v[0])
|
34 |
+
|
35 |
+
return pub_nodes
|
36 |
+
|
37 |
+
def get_route_length(self, p0, p1):
|
38 |
+
# Find length in meters of shortest path from pub p0 to pub p1
|
39 |
+
route = nx.shortest_path(self.G, self.pub_nodes[p0], self.pub_nodes[p1], weight='length')
|
40 |
+
route_lengths = ox.utils_graph.get_route_edge_attributes(self.G, route, attribute = 'length')
|
41 |
+
route_length_total = sum(route_lengths)
|
42 |
+
return route_length_total
|
43 |
+
|
44 |
+
def create_distance_matrix(self, pubs_considered):
|
45 |
+
distance_matrix = []
|
46 |
+
for i in range(len(pubs_considered)):
|
47 |
+
row = []
|
48 |
+
for j in range(len(pubs_considered)):
|
49 |
+
distance = self.get_route_length(pubs_considered[i], pubs_considered[j])
|
50 |
+
row.append(round(distance*1000)) # avoids rounding error in Google's OR-Tools package
|
51 |
+
distance_matrix.append(row)
|
52 |
+
|
53 |
+
return distance_matrix
|
54 |
+
|
55 |
+
def plot_map(self):
|
56 |
+
node_colours = ['#FF0000' if i in list(self.pub_nodes.values()) else '#999999' for i in self.G.nodes]
|
57 |
+
fig, ax = ox.plot_graph(self.G, bgcolor='#FFFFFF', node_color=node_colours, show=False, close=False)
|
58 |
+
for _, node in ox.graph_to_gdfs(self.G, nodes=True, edges=False).fillna("").iterrows():
|
59 |
+
for k, v in self.pub_nodes.items():
|
60 |
+
if node.name == v:
|
61 |
+
c = node["geometry"].centroid
|
62 |
+
ax.annotate(k, xy=(c.x, c.y), xycoords='data', xytext=(3, -2), textcoords='offset points', size=8)
|
63 |
+
plt.show()
|
64 |
+
|
65 |
+
def get_route_nodes(self, route):
|
66 |
+
# Find intermediary route nodes for plotting
|
67 |
+
route_nodes = []
|
68 |
+
for i in range(len(route)-1):
|
69 |
+
path = nx.shortest_path(self.G, self.pub_nodes[route[i]], self.pub_nodes[route[i+1]], weight='length')
|
70 |
+
route_nodes.append(path)
|
71 |
+
return route_nodes
|
72 |
+
|
73 |
+
def plot_route(self, route):
|
74 |
+
route_nodes = self.get_route_nodes(route)
|
75 |
+
fig, ax = ox.plot_graph_routes(self.G, route_nodes, bgcolor='#FFFFFF', show=False, close=False,
|
76 |
+
figsize=(12, 12))
|
77 |
+
for _, node in ox.graph_to_gdfs(self.G, nodes=True, edges=False).fillna("").iterrows():
|
78 |
+
for i, k in enumerate(route):
|
79 |
+
if node.name == self.pub_nodes[k]:
|
80 |
+
c = node["geometry"].centroid
|
81 |
+
ax.annotate(f'{i}: {k}', xy=(c.x, c.y), xycoords='data', xytext=(3, -2), textcoords='offset points', size=8)
|
82 |
+
|
83 |
+
return fig
|
84 |
+
|
85 |
+
def create_data(self, start, pubs_considered):
|
86 |
+
data = {}
|
87 |
+
start_index = pubs_considered.index(start)
|
88 |
+
data['distance_matrix'] = self.create_distance_matrix(pubs_considered)
|
89 |
+
data['num_vehicles'] = 1
|
90 |
+
data['depot'] = start_index
|
91 |
+
return data
|
92 |
+
|
93 |
+
def format_solution(self, manager, routing, solution, pubs_considered):
|
94 |
+
"""Formats solution for osmnx plotting"""
|
95 |
+
index = routing.Start(0)
|
96 |
+
route = [index]
|
97 |
+
|
98 |
+
leg_distances = []
|
99 |
+
route_distance = 0
|
100 |
+
while not routing.IsEnd(index):
|
101 |
+
previous_index = index
|
102 |
+
index = solution.Value(routing.NextVar(index))
|
103 |
+
if route[0] == manager.IndexToNode(index):
|
104 |
+
break
|
105 |
+
route.append(manager.IndexToNode(index))
|
106 |
+
leg_distance = routing.GetArcCostForVehicle(previous_index, index, 0)
|
107 |
+
leg_distances.append(leg_distance)
|
108 |
+
route_distance += leg_distance
|
109 |
+
|
110 |
+
self.optimal_route = [pubs_considered[i] for i in route]
|
111 |
+
self.optimal_distance = round(route_distance/1000)
|
112 |
+
|
113 |
+
def optimise(self, start_point, pubs_considered):
|
114 |
+
|
115 |
+
def distance_callback(from_index, to_index):
|
116 |
+
"""Returns the distance between the two nodes."""
|
117 |
+
# Convert from routing variable Index to distance matrix NodeIndex
|
118 |
+
from_node = manager.IndexToNode(from_index)
|
119 |
+
to_node = manager.IndexToNode(to_index)
|
120 |
+
return data['distance_matrix'][from_node][to_node]
|
121 |
+
|
122 |
+
data = self.create_data(start_point, pubs_considered)
|
123 |
+
|
124 |
+
# Create the routing index manager
|
125 |
+
manager = pywrapcp.RoutingIndexManager(len(data['distance_matrix']),
|
126 |
+
data['num_vehicles'], data['depot'])
|
127 |
+
|
128 |
+
# Create Routing Model
|
129 |
+
routing = pywrapcp.RoutingModel(manager)
|
130 |
+
|
131 |
+
transit_callback_index = routing.RegisterTransitCallback(distance_callback)
|
132 |
+
|
133 |
+
# Define cost of each arc
|
134 |
+
routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
|
135 |
+
|
136 |
+
# Settings for simualted annealing
|
137 |
+
search_parameters = pywrapcp.DefaultRoutingSearchParameters()
|
138 |
+
search_parameters.local_search_metaheuristic = (
|
139 |
+
routing_enums_pb2.LocalSearchMetaheuristic.SIMULATED_ANNEALING)
|
140 |
+
search_parameters.time_limit.seconds = 5
|
141 |
+
search_parameters.log_search = True
|
142 |
+
|
143 |
+
# Solve the problem
|
144 |
+
solution = routing.SolveWithParameters(search_parameters)
|
145 |
+
|
146 |
+
# Format solution
|
147 |
+
if solution:
|
148 |
+
self.format_solution(manager, routing, solution, pubs_considered)
|
149 |
+
|
150 |
+
if __name__=='main':
|
151 |
+
df = pd.read_csv('galway_pubs.csv')
|
152 |
+
G = ox.io.load_graphml('galway.graphml')
|
153 |
+
|
154 |
+
crawler = pub_crawl(df, G)
|
155 |
+
|
156 |
+
print('Locations of pubs in Galway to consider:')
|
157 |
+
crawler.plot_map()
|
158 |
+
|
159 |
+
initial_route = crawler.initial_route
|
160 |
+
|
161 |
+
print('Initial route before optimisation (default pub ordering):')
|
162 |
+
crawler.plot_route(initial_route)
|
163 |
+
|
164 |
+
start_pub = 'The Sliding Rock'
|
165 |
+
crawler.optimise(start_pub, ['The Sliding Rock', 'The Salt House', 'Caribou'])
|
166 |
+
|
167 |
+
optimal_route = crawler.optimal_route
|
168 |
+
print('Optimised route:')
|
169 |
+
print(f'Route distance: {crawler.optimal_distance} meters')
|
170 |
+
crawler.plot_route(optimal_route)
|
171 |
+
|
172 |
+
|