Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/route/routecompare.py
169674 views
1
#!/usr/bin/env python
2
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3
# Copyright (C) 2008-2025 German Aerospace Center (DLR) and others.
4
# This program and the accompanying materials are made available under the
5
# terms of the Eclipse Public License 2.0 which is available at
6
# https://www.eclipse.org/legal/epl-2.0/
7
# This Source Code may also be made available under the following Secondary
8
# Licenses when the conditions for such availability set forth in the Eclipse
9
# Public License 2.0 are satisfied: GNU General Public License, version 2
10
# or later which is available at
11
# https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
12
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
13
14
# @file routecompare.py
15
# @author Michael Behrisch
16
# @author Daniel Krajzewicz
17
# @date 2008-03-25
18
19
"""
20
This script compares two route sets by calculating
21
a similarity for any two routes based on the number of common edges
22
and determining a maximum weighted matching between the route sets.
23
It needs at least two parameters, which are the route sets to compare.
24
Optionally a district file may be given, then only routes with
25
the same origin and destination district are matched.
26
"""
27
from __future__ import absolute_import
28
from __future__ import print_function
29
import os
30
import sys
31
import array
32
from xml.sax import make_parser, handler
33
sys.path.append(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
34
from sumolib.options import ArgumentParser # noqa
35
36
SCALE = 10000
37
INFINITY = 2**30
38
39
40
class RouteReader(handler.ContentHandler):
41
42
def __init__(self, routeMap, edges):
43
self._routes = routeMap
44
self._edges = edges
45
self._vID = ''
46
self._routeID = ''
47
self._routeString = ''
48
49
def startElement(self, name, attrs):
50
if name == 'vehicle':
51
self._vID = attrs['id']
52
elif name == 'route':
53
if 'id' in attrs:
54
self._routeID = attrs['id']
55
else:
56
self._routeID = self._vID
57
self._vID = ''
58
self._routeString = ''
59
if 'edges' in attrs:
60
self._routeString = attrs['edges']
61
62
def endElement(self, name):
63
if name == 'route':
64
route = array.array('L')
65
for edge in self._routeString.split():
66
if edge not in self._edges:
67
self._edges[edge] = len(self._edges)
68
route.append(self._edges[edge])
69
self._routes[self._routeID] = route
70
71
def characters(self, content):
72
if self._routeID != '':
73
self._routeString += content
74
75
76
class DistrictReader(handler.ContentHandler):
77
78
def __init__(self, sourceEdges, sinkEdges, edges):
79
self._sources = sourceEdges
80
self._sinks = sinkEdges
81
self._edges = edges
82
self._districtID = ''
83
84
def startElement(self, name, attrs):
85
if name == 'taz':
86
self._districtID = attrs['id']
87
elif name == 'tazSource':
88
if attrs['id'] in self._edges:
89
self._sources[self._edges[attrs['id']]] = self._districtID
90
else:
91
if options.verbose:
92
print("Warning! No routes touching source edge %s of %s." % (
93
attrs['id'], self._districtID))
94
elif name == 'tazSink':
95
if attrs['id'] in self._edges:
96
self._sinks[self._edges[attrs['id']]] = self._districtID
97
else:
98
if options.verbose:
99
print("Warning! No routes touching sink edge %s of %s." %
100
(attrs['id'], self._districtID))
101
102
103
def compare(first, second):
104
commonEdges = 0
105
for edge in first:
106
if edge in second:
107
commonEdges += SCALE
108
return commonEdges // max(len(first), len(second))
109
110
111
def matching(routeIDs1, routeIDs2, similarityMatrix, match):
112
matchVal = 0
113
for id1 in routeIDs1:
114
maxMatch = -1
115
matchId = ""
116
for id2 in routeIDs2:
117
if id2 not in match and similarityMatrix[id1][id2] > maxMatch:
118
maxMatch = similarityMatrix[id1][id2]
119
matchId = id2
120
if matchId:
121
match[matchId] = id1
122
matchVal += maxMatch
123
return matchVal
124
125
126
def identityCount(routeIDs1, routeIDs2, similarityMatrix):
127
matched = set()
128
for id1 in routeIDs1:
129
for id2 in routeIDs2:
130
if id2 not in matched and similarityMatrix[id1][id2] == SCALE:
131
matched.add(id2)
132
break
133
return len(matched)
134
135
136
class Node:
137
138
def __init__(self, routeID, weight):
139
self.routeID = routeID
140
self.weight = weight
141
self.eps = INFINITY
142
self.level = INFINITY
143
self.match = None
144
145
146
def augmentSimultan(vTerm):
147
dead = set()
148
for v in vTerm:
149
path = [v]
150
id = 0
151
while True:
152
while len(path[id].pre) > 0 and path[id].pre[0] in dead:
153
path[id].pre.pop(0)
154
if len(path[id].pre) == 0:
155
if id == 0:
156
break
157
dead.add(path[id - 1])
158
id -= 2
159
else:
160
if id == len(path) - 1:
161
path.append(None)
162
path[id + 1] = path[id].pre.pop(0)
163
dead.add(path[id + 1])
164
id += 1
165
if path[id].level == 0:
166
for j in range(0, id + 1, 2):
167
path[j].match = path[j + 1]
168
path[j + 1].match = path[j]
169
break
170
else:
171
if id == len(path) - 1:
172
path.append(None)
173
path[id + 1] = path[id].match
174
id += 1
175
176
177
def hungarianDAG(U, V, similarityMatrix):
178
while True:
179
S = set()
180
T = set()
181
Q = []
182
vTerm = set()
183
for u in U:
184
u.level = INFINITY
185
u.eps = INFINITY
186
if not u.match:
187
S.add(u)
188
u.level = 0
189
Q.append(u)
190
for v in V:
191
v.level = INFINITY
192
v.eps = INFINITY
193
while len(Q) > 0:
194
s = Q.pop(0)
195
for t in V:
196
if s.weight + t.weight == similarityMatrix[s.routeID][t.routeID]:
197
if t.level > s.level:
198
if t.level == INFINITY:
199
T.add(t)
200
t.level = s.level + 1
201
t.pre = [s]
202
if not t.match:
203
vTerm.add(t)
204
else:
205
S.add(t.match)
206
t.match.level = t.level + 1
207
Q.append(t.match)
208
else:
209
t.pre.append(s)
210
else:
211
t.eps = min(
212
t.eps, s.weight + t.weight - similarityMatrix[s.routeID][t.routeID])
213
if len(vTerm) > 0:
214
break
215
epsilon = INFINITY
216
for t in V:
217
if t.eps < epsilon:
218
epsilon = t.eps
219
if epsilon == INFINITY:
220
break
221
for x in S:
222
x.weight -= epsilon
223
for x in T:
224
x.weight += epsilon
225
return vTerm
226
227
228
def maxMatching(routeIDs1, routeIDs2, similarityMatrix, match):
229
maxSimilarity = 0
230
for id1 in routeIDs1:
231
for value in similarityMatrix[id1].values():
232
if value > maxSimilarity:
233
maxSimilarity = value
234
U = []
235
for id1 in routeIDs1:
236
U.append(Node(id1, maxSimilarity))
237
V = []
238
for id2 in routeIDs2:
239
V.append(Node(id2, 0))
240
while True:
241
vTerm = hungarianDAG(U, V, similarityMatrix)
242
if len(vTerm) == 0:
243
break
244
augmentSimultan(vTerm)
245
matchVal = 0
246
for v in V:
247
if v.match:
248
match[v.routeID] = v.match.routeID
249
matchVal += similarityMatrix[v.match.routeID][v.routeID]
250
return matchVal
251
252
253
ap = ArgumentParser(usage="usage: %prog [options] <routes1> <routes2>")
254
ap.add_argument("-d", "--districts-file", dest="districts", category="input", type=ap.file,
255
default="", help="read districts from FILE", metavar="FILE")
256
ap.add_argument("-s", "--simple-match", action="store_true", dest="simple",
257
default=False, help="use simple matching algorithm")
258
ap.add_argument("-p", "--print-matching", action="store_true", dest="printmatch",
259
default=False, help="print the resulting matching")
260
ap.add_argument("-v", "--verbose", action="store_true", dest="verbose",
261
default=False, help="print more info")
262
ap.add_argument("routes1", category="input", type=ap.route_file, help="first route file of comparison")
263
ap.add_argument("routes2", category="input", type=ap.route_file, help="second route file of comparison")
264
options = ap.parse_args()
265
266
edges = {}
267
routes1 = {}
268
routes2 = {}
269
parser = make_parser()
270
if options.verbose:
271
print("Reading first routes file %s" % options.routes1)
272
parser.setContentHandler(RouteReader(routes1, edges))
273
parser.parse(options.routes1)
274
if options.verbose:
275
print("Reading second routes file %s" % options.routes2)
276
parser.setContentHandler(RouteReader(routes2, edges))
277
parser.parse(options.routes2)
278
279
routeMatrix1 = {}
280
routeMatrix2 = {}
281
if options.districts:
282
sources = {}
283
sinks = {}
284
if options.verbose:
285
print("Reading districts %s" % options.districts)
286
parser.setContentHandler(DistrictReader(sources, sinks, edges))
287
parser.parse(options.districts)
288
for routes, routeMatrix in [(routes1, routeMatrix1), (routes2, routeMatrix2)]:
289
for routeID in sorted(routes):
290
route = routes[routeID]
291
source = sources[route[0]]
292
sink = sinks[route[-1]]
293
if source not in routeMatrix:
294
routeMatrix[source] = {}
295
if sink not in routeMatrix[source]:
296
routeMatrix[source][sink] = []
297
routeMatrix[source][sink].append(routeID)
298
else:
299
for routes, routeMatrix in [(routes1, routeMatrix1), (routes2, routeMatrix2)]:
300
routeMatrix["dummySource"] = {}
301
routeMatrix["dummySource"]["dummySink"] = list(sorted(routes.keys()))
302
303
match = {}
304
totalMatch = 0
305
totalIdentical = 0
306
for source in sorted(routeMatrix1):
307
if source not in routeMatrix2:
308
if options.verbose:
309
print(
310
"Warning! No routes starting at %s in second route set" % source)
311
continue
312
for sink in sorted(routeMatrix1[source]):
313
routeIDs1 = routeMatrix1[source][sink]
314
if sink not in routeMatrix2[source]:
315
if options.verbose:
316
print("Warning! No routes starting at %s and ending at %s in second route set" % (
317
source, sink))
318
continue
319
routeIDs2 = routeMatrix2[source][sink]
320
if options.verbose and len(routeIDs1) != len(routeIDs2):
321
print("Warning! Different route set sizes for start '%s' and end '%s'." % (
322
source, sink))
323
similarityMatrix = {}
324
for idx, id1 in enumerate(routeIDs1):
325
for oldID in routeIDs1[:idx]:
326
if routes1[oldID] == routes1[id1]:
327
similarityMatrix[id1] = similarityMatrix[oldID]
328
break
329
if id1 not in similarityMatrix:
330
similarityMatrix[id1] = {}
331
for id2 in routeIDs2:
332
similarityMatrix[id1][id2] = compare(
333
routes1[id1], routes2[id2])
334
if options.simple:
335
matchVal = matching(routeIDs1, routeIDs2, similarityMatrix, match)
336
else:
337
matchVal = maxMatching(
338
routeIDs1, routeIDs2, similarityMatrix, match)
339
totalMatch += matchVal
340
identityVal = identityCount(routeIDs1, routeIDs2, similarityMatrix)
341
totalIdentical += identityVal
342
if options.verbose:
343
print(source, sink, float(matchVal) / len(routeIDs1) / SCALE,
344
float(identityVal) / len(routeIDs1))
345
if options.printmatch:
346
for r2, r1 in sorted(match.items()):
347
print(r1, r2)
348
print(float(totalMatch) / len(routes1) / SCALE,
349
float(totalIdentical) / len(routes1))
350
351