Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/import/visum/visum_convertXMLRoutes.py
185787 views
1
#!/usr/bin/env python
2
# -*- coding: utf-8 -*-
3
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
4
# Copyright (C) 2015-2025 German Aerospace Center (DLR) and others.
5
# This program and the accompanying materials are made available under the
6
# terms of the Eclipse Public License 2.0 which is available at
7
# https://www.eclipse.org/legal/epl-2.0/
8
# This Source Code may also be made available under the following Secondary
9
# Licenses when the conditions for such availability set forth in the Eclipse
10
# Public License 2.0 are satisfied: GNU General Public License, version 2
11
# or later which is available at
12
# https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
13
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
14
15
# @file visum_convertXMLRoutes.py
16
# @author Jakob Erdmann
17
# @date Nov 17 2025
18
19
"""
20
Parses a VISUM xml-route file and writes a SUMO route file
21
"""
22
23
from __future__ import print_function
24
from __future__ import absolute_import
25
import os
26
import sys
27
try:
28
from functools import cache
29
except ImportError:
30
# python < 3.9
31
from functools import lru_cache as cache
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.miscutils import openz # noqa
37
38
MSG_CACHE = set()
39
40
41
def get_options(args=None):
42
op = sumolib.options.ArgumentParser(description="Import VISUM route file")
43
# input
44
op.add_argument("-n", "--net-file", category="input", dest="netfile", required=True, type=op.net_file,
45
help="define the net file (mandatory)")
46
op.add_argument("-r", "--route-file", category="input", dest="routefile", required=True, type=op.route_file,
47
help="define the net file (mandatory)")
48
# output
49
op.add_argument("-o", "--output-trip-file", category="output", dest="outfile", required=True, type=op.route_file,
50
help="define the output route file")
51
# processing
52
op.add_argument("--scale", metavar="FLOAT", type=float, default=1,
53
help="Scale volume by the given value (i.e. 24 when volume denotes hourly rather than daily traffic)") # noqa
54
op.add_argument("--vclass", help="Only include routes for the given vclass")
55
op.add_argument("-a", "--attributes", default="",
56
help="additional flow attributes.")
57
58
options = op.parse_args(args=args)
59
return options
60
61
62
def append_no_duplicate(edges, e):
63
if edges and edges[-1] == e:
64
return
65
edges.append(e)
66
67
68
@cache
69
def repairEdgeEdge(net, prevEdge, edge, vClass):
70
if edge in prevEdge.getAllowedOutgoing(vClass):
71
return [prevEdge, edge], None
72
return net.getFastestPath(prevEdge, edge, vClass=vClass)
73
74
75
@cache
76
def repairEdgeNode(net, prevEdge, node, vClass):
77
bestPath = None
78
bestCost = 1e400
79
for edge in node.getIncoming():
80
path, cost = net.getFastestPath(prevEdge, edge, vClass=vClass)
81
if path and cost < bestCost:
82
bestPath = path
83
bestCost = cost
84
return bestPath, bestCost
85
86
87
@cache
88
def repairNodeNode(net, prevNode, node, vClass):
89
bestPath = None
90
bestCost = 1e400
91
for prevEdge in prevNode.getOutgoing():
92
for edge in node.getIncoming():
93
path, cost = net.getFastestPath(prevEdge, edge, vClass=vClass)
94
if path and cost < bestCost:
95
bestPath = path
96
bestCost = cost
97
return bestPath, bestCost
98
99
100
@cache
101
def repairNodeEdge(net, prevNode, edge, vClass):
102
bestPath = None
103
bestCost = 1e400
104
for prevEdge in prevNode.getOutgoing():
105
path, cost = net.getFastestPath(prevEdge, edge, vClass=vClass)
106
if path and cost < bestCost:
107
bestPath = path
108
bestCost = cost
109
return bestPath, bestCost
110
111
112
def getValidNodes(net, nodes):
113
"""obtain a stream of nodes that exist in the network"""
114
for nodeID in nodes:
115
if ' ' in nodeID:
116
nodeID, id2 = nodeID.split()
117
if id2[-1] == 'B':
118
continue
119
elif id2[-1] == 'A':
120
if net.hasNode(nodeID):
121
yield net.getNode(nodeID)
122
edgeID = id2[:-1]
123
if net.hasEdge(edgeID):
124
yield net.getEdge(edgeID).getToNode()
125
else:
126
if net.hasNode(nodeID):
127
yield net.getNode(nodeID)
128
if net.hasNode(id2):
129
yield net.getNode(id2)
130
else:
131
if net.hasNode(nodeID):
132
yield net.getNode(nodeID)
133
134
135
def getValidEdgesNodes(edgedict, validNodes):
136
"""obtain stream of edges intermixed with nodes that could not be assigned to edges"""
137
singleNodes = []
138
for node in validNodes:
139
if singleNodes:
140
e = edgedict.get((singleNodes[-1], node))
141
if e is not None:
142
singleNodes.pop()
143
yield singleNodes, e
144
singleNodes = []
145
else:
146
singleNodes.append(node)
147
else:
148
singleNodes.append(node)
149
if singleNodes:
150
yield singleNodes, None
151
152
153
def msgOnce(msg, key, file):
154
if key not in MSG_CACHE:
155
print(msg, file=file)
156
MSG_CACHE.add(key)
157
158
159
def getConnectedEdges(net, validEdgesNodes, vClass, routeID):
160
"""obtain a sequence of connected edges"""
161
result = []
162
lastEdge = None
163
lastNode = None
164
msgSuccess = "Route %s:" % routeID + " Repaired path between %s, length %s, cost %s"
165
msgFail = "Route %s:" % routeID + " Found no path between %s"
166
for singleNodes, edge in validEdgesNodes:
167
for n in singleNodes:
168
if lastEdge:
169
path, cost = repairEdgeNode(net, lastEdge, n, vClass)
170
between = "edge %s and node %s" % (lastEdge.getID(), n.getID())
171
if path:
172
if len(path) > 2:
173
msgOnce(msgSuccess % (between, len(path), cost), between, sys.stderr)
174
result += path[1:]
175
lastEdge = path[-1]
176
else:
177
print(msgFail % between, file=sys.stderr)
178
return None
179
elif lastNode:
180
path, cost = repairNodeNode(net, lastNode, n, vClass)
181
between = "node %s and node %s" % (lastNode.getID(), n.getID())
182
if path:
183
msgOnce(msgSuccess % (between, len(path), cost), between, sys.stderr)
184
result += path
185
lastEdge = path[-1]
186
else:
187
print(msgFail % between, file=sys.stderr)
188
return None
189
else:
190
lastNode = n
191
if edge is not None:
192
if lastEdge:
193
path, cost = repairEdgeEdge(net, lastEdge, edge, vClass)
194
between = "edge %s and edge %s" % (lastEdge.getID(), edge.getID())
195
if path:
196
if len(path) > 2:
197
msgOnce(msgSuccess % (between, len(path), cost), between, sys.stderr)
198
result += path[1:]
199
lastEdge = path[-1]
200
else:
201
print(msgFail % between, file=sys.stderr)
202
return None
203
elif lastNode:
204
path, cost = repairNodeEdge(net, lastNode, edge, vClass)
205
between = "node %s and edge %s" % (lastNode.getID(), edge.getID())
206
if path:
207
msgOnce(msgSuccess % (between, len(path), cost), between, sys.stderr)
208
result += path
209
lastEdge = path[-1]
210
else:
211
print(msgFail % between, file=sys.stderr)
212
return None
213
else:
214
result.append(edge)
215
lastEdge = edge
216
217
return result
218
219
220
def main(options):
221
vTypes = dict()
222
nSkipped = 0
223
nBroken = 0
224
nDisallowed = 0
225
nZeroFlows = 0
226
nZeroRoutes = 0
227
net = sumolib.net.readNet(options.netfile)
228
allowed = set()
229
if options.vclass:
230
allowed.add(None)
231
for e in net.getEdges():
232
if e.allows(options.vclass):
233
allowed.add(e)
234
235
edgedict = {} # (from,to) -> edge
236
for e in net.getEdges():
237
edgedict[e.getFromNode(), e.getToNode()] = e
238
239
with openz(options.outfile, 'w') as fout:
240
sumolib.writeXMLHeader(fout, "$Id$", "routes", options=options)
241
for vtype in sumolib.xml.parse_fast(options.routefile, 'VEHTYPETI',
242
['INDEX', 'FROMTIME', 'TOTIME', 'VEHTYPEID']):
243
vTypes[vtype.INDEX] = (vtype.VEHTYPEID, vtype.FROMTIME, vtype.TOTIME)
244
fout.write(' <vType id="%s"/>\n' % vtype.VEHTYPEID)
245
246
nested = {
247
'ITEM': ['NODE'],
248
'DEMAND': ['VTI', 'VOLUME']}
249
for route in sumolib.xml.parse_fast_structured(options.routefile, 'ROUTE', ['INDEX'], nested):
250
nodes = [i.NODE for i in route.ITEM]
251
validNodes = list(getValidNodes(net, nodes))
252
if len(validNodes) < 2:
253
nSkipped += 1
254
continue
255
validEdgesNodes = list(getValidEdgesNodes(edgedict, validNodes))
256
if options.vclass:
257
if any([e not in allowed for s, e in validEdgesNodes]):
258
nDisallowed += 1
259
continue
260
edges = getConnectedEdges(net, validEdgesNodes, options.vclass, route.INDEX)
261
if not edges:
262
nBroken += 1
263
continue
264
265
totalVolume = 0
266
for demand in route.DEMAND:
267
totalVolume += float(demand.VOLUME)
268
if totalVolume == 0:
269
nZeroRoutes += 1
270
continue
271
272
edgeIDs = [e.getID() for e in edges]
273
274
fout.write(' <route id="%s" edges="%s"/>\n' % (route.INDEX, ' '.join(edgeIDs)))
275
for demand in route.DEMAND:
276
flowID = "%s_%s" % (route.INDEX, demand.VTI)
277
vtype, begin, end = vTypes[demand.VTI]
278
# assume VOLUME is per day
279
rate = float(demand.VOLUME) * options.scale / (3600 * 24)
280
if rate > 0:
281
attrs = ""
282
if options.attributes:
283
attrs = " " + options.attributes
284
fout.write(' <flow id="%s" route="%s" type="%s" begin="%s" end="%s" period="exp(%s)"%s/>\n' % (
285
flowID, route.INDEX, vtype, begin, end, rate, attrs))
286
else:
287
nZeroFlows += 1
288
fout.write("</routes>\n")
289
290
if nSkipped > 0:
291
print("Warning: Skipped %s routes because they were defined with a single node" % nSkipped)
292
if nBroken > 0:
293
print("Warning: Skipped %s routes because they could not be repaired" % nBroken)
294
if nZeroRoutes > 0:
295
print("Warning: Skipped %s routes because they have no volume" % nZeroRoutes)
296
if nZeroFlows > 0:
297
print("Warning: Skipped %s flows because they have no volume" % nZeroFlows)
298
if nDisallowed > 0:
299
print("Warning: Ignored %s routes because they have edges that are not allowed for %s " % (
300
nDisallowed, options.vclass))
301
302
303
if __name__ == "__main__":
304
main(get_options())
305
306