Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/route/implausibleRoutes.py
169673 views
1
#!/usr/bin/env python
2
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3
# Copyright (C) 2014-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 implausibleRoutes.py
15
# @author Jakob Erdmann
16
# @date 2017-03-28
17
18
"""
19
Find routes that are implausible due to:
20
- being longer than the shortest path between the first and last edge
21
- being longer than the air-distance between the first and the last edge
22
23
The script computes an implausibility-score from configurable factors and
24
reports all routes above the specified threshold.
25
"""
26
from __future__ import absolute_import
27
from __future__ import print_function
28
29
import os
30
import sys
31
import subprocess
32
33
if 'SUMO_HOME' in os.environ:
34
sys.path.append(os.path.join(os.environ['SUMO_HOME'], 'tools'))
35
import sumolib # noqa
36
from sumolib.xml import parse, parse_fast_nested # noqa
37
from sumolib.net import readNet # noqa
38
from sumolib.miscutils import Statistics, euclidean, Colorgen # noqa
39
from sumolib.options import ArgumentParser
40
from route2poly import generate_poly # noqa
41
else:
42
sys.exit("please declare environment variable 'SUMO_HOME'")
43
44
45
def get_options():
46
USAGE = "Usage " + sys.argv[0] + " [options] <net.xml> <rou.xml>"
47
optParser = ArgumentParser(usage=USAGE)
48
optParser.add_option("-a", "--additional-files", default=None, dest='additional',
49
help="additional files to pass through duarouter e.g. vehicle type definitions")
50
optParser.add_option("--unsorted-input", default=False, dest='unsortedinput', action="store_true",
51
help="If the provided route file has unsorted departure times")
52
optParser.add_option("--ignore-errors", default=False, dest='duaroutererrors', action="store_true",
53
help="Ignore errors when calling duarouter")
54
optParser.add_option("-v", "--verbose", action="store_true",
55
default=False, help="Give more output")
56
optParser.add_option("--threshold", type=float, default=2.5,
57
help="Routes with an implausibility-score above threshold are reported")
58
optParser.add_option("--airdist-ratio-factor", type=float, default=1, dest="airdist_ratio_factor",
59
help="Implausibility factor for the ratio of routeDist/airDist ")
60
optParser.add_option("--detour-ratio-factor", type=float, default=1, dest="detour_ratio_factor",
61
help="Implausibility factor for the ratio of routeDuration/shortestDuration ")
62
optParser.add_option("--detour-factor", type=float, default=0.01, dest="detour_factor",
63
help="Implausibility factor for the absolute detour time in (routeDuration-shortestDuration)" +
64
" in seconds")
65
optParser.add_option("--min-dist", type=float, default=0, dest="min_dist",
66
help="Minimum shortest-path distance below which routes are implausible")
67
optParser.add_option("--min-air-dist", type=float, default=0, dest="min_air_dist",
68
help="Minimum air distance below which routes are implausible")
69
optParser.add_option("--standalone", action="store_true", default=False,
70
help="Parse stand-alone routes that are not define as child-element of a vehicle")
71
optParser.add_option("--blur", type=float, default=0,
72
help="maximum random disturbance to output polygon geometry")
73
optParser.add_option("--ignore-routes", dest="ignore_routes",
74
help="List of route IDs (one per line) that are filtered when generating polygons and " +
75
"command line output (they will still be added to restrictions-output)")
76
optParser.add_option("-o", "--xml-output", dest="xmlOutput",
77
help="Write implausibility scores and routes to xml FILE")
78
optParser.add_option("--restriction-output", dest="restrictions_output",
79
help="Write flow-restriction output suitable for passing to flowrouter.py to FILE")
80
optParser.add_option("--od-restrictions", action="store_true", dest="odrestrictions", default=False,
81
help="Write restrictions for origin-destination relations rather than whole routes")
82
optParser.add_option("--edge-loops", action="store_true",
83
default=False, help="report routes which use edges twice")
84
optParser.add_option("--node-loops", action="store_true",
85
default=False, help="report routes which use junctions twice")
86
optParser.add_option("--threads", default=1, type=int,
87
help="number of threads to use for duarouter")
88
optParser.add_option("--min-edges", default=2, type=int,
89
help="number of edges a route needs to have to be analyzed")
90
optParser.add_option("--heterogeneous", action="store_true",
91
default=False, help="Use slow parsing for route files with different formats in one file")
92
optParser.add_option("--reuse-routing", action="store_true",
93
default=False, help="do not run duarouter again if output file exists")
94
optParser.add_option("network", help="network file to use")
95
optParser.add_option("routeFiles", nargs='+', help="route files to use")
96
options = optParser.parse_args()
97
98
# options for generate_poly
99
options.layer = 100
100
options.geo = False
101
options.internal = False
102
options.spread = None
103
104
return options
105
106
107
class RouteInfo:
108
def __init__(self, route):
109
self.edges = route.edges.split()
110
111
112
def calcDistAndLoops(rInfo, net, options):
113
if net.hasInternal:
114
rInfo.airDist = euclidean(
115
net.getEdge(rInfo.edges[0]).getShape()[0],
116
net.getEdge(rInfo.edges[-1]).getShape()[-1])
117
else:
118
rInfo.airDist = euclidean(
119
net.getEdge(rInfo.edges[0]).getFromNode().getCoord(),
120
net.getEdge(rInfo.edges[-1]).getToNode().getCoord())
121
rInfo.length = sumolib.route.getLength(net, rInfo.edges)
122
rInfo.airDistRatio = rInfo.length / rInfo.airDist
123
rInfo.edgeLoop = False
124
rInfo.nodeLoop = False
125
if options.edge_loops:
126
seen = set()
127
for e in rInfo.edges:
128
if e in seen:
129
rInfo.edgeLoop = True
130
rInfo.nodeLoop = True
131
break
132
seen.add(e)
133
if options.node_loops and not rInfo.nodeLoop:
134
seen = set()
135
for e in rInfo.edges:
136
t = net.getEdge(e).getToNode()
137
if t in seen:
138
rInfo.nodeLoop = True
139
break
140
seen.add(t)
141
142
143
def addOrSkip(routeInfos, skipped, rid, route, min_edges):
144
ri = RouteInfo(route)
145
if len(ri.edges) >= min_edges:
146
routeInfos[rid] = ri
147
else:
148
skipped.add(rid)
149
150
151
def main():
152
options = get_options()
153
if options.verbose:
154
print("parsing network from", options.network)
155
net = readNet(options.network, withInternal=True)
156
read = 0
157
routeInfos = {} # id-> RouteInfo
158
skipped = set()
159
for routeFile in options.routeFiles:
160
if options.verbose:
161
print("parsing routes from", routeFile)
162
idx = 0
163
if options.standalone:
164
for idx, route in enumerate(parse(routeFile, 'route')):
165
if options.verbose and idx > 0 and idx % 100000 == 0:
166
print(idx, "routes read")
167
addOrSkip(routeInfos, skipped, route.id, route, options.min_edges)
168
else:
169
if options.heterogeneous:
170
for idx, vehicle in enumerate(parse(routeFile, 'vehicle')):
171
if options.verbose and idx > 0 and idx % 100000 == 0:
172
print(idx, "vehicles read")
173
addOrSkip(routeInfos, skipped, vehicle.id, vehicle.route[0], options.min_edges)
174
else:
175
prev = (None, None)
176
for vehicle, route in parse_fast_nested(routeFile, 'vehicle', 'id', 'route', 'edges'):
177
if prev[0] != vehicle.id:
178
if options.verbose and idx > 0 and idx % 500000 == 0:
179
print(idx, "vehicles read")
180
if prev[0] is not None:
181
addOrSkip(routeInfos, skipped, prev[0], prev[1], options.min_edges)
182
prev = (vehicle.id, route)
183
idx += 1
184
if prev[0] is not None:
185
addOrSkip(routeInfos, skipped, prev[0], prev[1], options.min_edges)
186
read += idx
187
if options.verbose:
188
print(read, "routes read", len(skipped), "short routes skipped")
189
190
if options.verbose:
191
print("calculating air distance and checking loops")
192
for idx, ri in enumerate(routeInfos.values()):
193
if options.verbose and idx > 0 and idx % 100000 == 0:
194
print(idx, "routes checked")
195
calcDistAndLoops(ri, net, options)
196
197
prefix = os.path.commonprefix(options.routeFiles)
198
duarouterOutput = prefix + '.rerouted.rou.xml'
199
duarouterAltOutput = prefix + '.rerouted.rou.alt.xml'
200
if os.path.exists(duarouterAltOutput) and options.reuse_routing:
201
if options.verbose:
202
print("reusing old duarouter file", duarouterAltOutput)
203
else:
204
if options.standalone:
205
duarouterInput = prefix
206
# generate suitable input file for duarouter
207
duarouterInput += ".vehRoutes.xml"
208
with open(duarouterInput, 'w') as outf:
209
outf.write('<routes>\n')
210
for rID, rInfo in routeInfos.items():
211
outf.write(' <vehicle id="%s" depart="0">\n' % rID)
212
outf.write(' <route edges="%s"/>\n' % ' '.join(rInfo.edges))
213
outf.write(' </vehicle>\n')
214
outf.write('</routes>\n')
215
else:
216
duarouterInput = ",".join(options.routeFiles)
217
218
command = [sumolib.checkBinary('duarouter'), '-n', options.network,
219
'-r', duarouterInput, '-o', duarouterOutput,
220
'--no-step-log', '--routing-threads', str(options.threads),
221
'--routing-algorithm', 'astar', '--aggregate-warnings', '1']
222
if options.additional is not None:
223
command += ['--additional-files', options.additional]
224
if options.unsortedinput is not False:
225
command += ['--unsorted-input', '1']
226
if options.duaroutererrors is not False:
227
command += ['--ignore-errors', '1']
228
229
if options.verbose:
230
command += ["-v"]
231
if options.verbose:
232
print("calling duarouter:", " ".join(command))
233
subprocess.call(command)
234
235
for vehicle in parse(duarouterAltOutput, 'vehicle'):
236
if vehicle.id in skipped:
237
continue
238
routeAlts = vehicle.routeDistribution[0].route
239
if len(routeAlts) == 1:
240
routeInfos[vehicle.id].detour = 0
241
routeInfos[vehicle.id].detourRatio = 1
242
routeInfos[vehicle.id].shortest_path_distance = routeInfos[vehicle.id].length
243
else:
244
oldCosts = float(routeAlts[0].cost)
245
newCosts = float(routeAlts[1].cost)
246
assert routeAlts[0].edges.split() == routeInfos[vehicle.id].edges
247
routeInfos[vehicle.id].shortest_path_distance = sumolib.route.getLength(net, routeAlts[1].edges.split())
248
if oldCosts <= newCosts:
249
routeInfos[vehicle.id].detour = 0
250
routeInfos[vehicle.id].detourRatio = 1
251
if oldCosts < newCosts:
252
sys.stderr.write(("Warning: fastest route for '%s' is slower than original route " +
253
"(old=%s, new=%s). Check vehicle types\n") % (
254
vehicle.id, oldCosts, newCosts))
255
else:
256
routeInfos[vehicle.id].detour = oldCosts - newCosts
257
routeInfos[vehicle.id].detourRatio = oldCosts / newCosts
258
259
implausible = []
260
allRoutesStats = Statistics("overall implausibility")
261
implausibleRoutesStats = Statistics("implausibility above threshold")
262
for rID in sorted(routeInfos.keys()):
263
ri = routeInfos[rID]
264
ri.implausibility = (options.airdist_ratio_factor * ri.airDistRatio +
265
options.detour_factor * ri.detour +
266
options.detour_ratio_factor * ri.detourRatio +
267
max(0, options.min_dist / ri.shortest_path_distance - 1) +
268
max(0, options.min_air_dist / ri.airDist - 1))
269
allRoutesStats.add(ri.implausibility, rID)
270
if ri.implausibility > options.threshold or ri.edgeLoop or ri.nodeLoop:
271
implausible.append((ri.implausibility, rID, ri))
272
implausibleRoutesStats.add(ri.implausibility, rID)
273
274
# generate restrictions
275
if options.restrictions_output is not None:
276
with open(options.restrictions_output, 'w') as outf:
277
for score, rID, ri in sorted(implausible):
278
edges = ri.edges
279
if options.odrestrictions and len(edges) > 2:
280
edges = [edges[0], edges[-1]]
281
outf.write("0 %s\n" % " ".join(edges))
282
283
# write xml output
284
if options.xmlOutput is not None:
285
with open(options.xmlOutput, 'w') as outf:
286
sumolib.writeXMLHeader(outf, "$Id$", options=options) # noqa
287
outf.write('<implausibleRoutes>\n')
288
for score, rID, ri in sorted(implausible):
289
edges = " ".join(ri.edges)
290
outf.write(' <route id="%s" edges="%s" score="%s"/>\n' % (
291
rID, edges, score))
292
outf.write('</implausibleRoutes>\n')
293
294
if options.ignore_routes is not None:
295
numImplausible = len(implausible)
296
ignored = set([r.strip() for r in open(options.ignore_routes)])
297
implausible = [r for r in implausible if r not in ignored]
298
print("Loaded %s routes to ignore. Reducing implausible from %s to %s" % (
299
len(ignored), numImplausible, len(implausible)))
300
301
# generate polygons
302
polyOutput = prefix + '.implausible.add.xml'
303
colorgen = Colorgen(("random", 1, 1))
304
with open(polyOutput, 'w') as outf:
305
outf.write('<additional>\n')
306
for score, rID, ri in sorted(implausible):
307
generate_poly(options, net, rID, colorgen(), ri.edges, outf, score)
308
outf.write('</additional>\n')
309
310
sys.stdout.write('score\troute\t(airDistRatio, detourRatio, detour, shortestDist, airDist, edgeLoop, nodeLoop)\n')
311
for score, rID, ri in sorted(implausible):
312
# , ' '.join(ri.edges)))
313
sys.stdout.write('%.7f\t%s\t%s\n' % (score, rID, (ri.airDistRatio, ri.detourRatio,
314
ri.detour, ri.shortest_path_distance,
315
ri.airDist, ri.edgeLoop, ri.nodeLoop)))
316
317
print(allRoutesStats)
318
print(implausibleRoutesStats)
319
320
321
if __name__ == "__main__":
322
main()
323
324