Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/plot_trajectories.py
169659 views
1
#!/usr/bin/env python
2
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3
# Copyright (C) 2007-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 plot_trajectories.py
15
# @author Jakob Erdmann
16
# @author Mirko Barthauer
17
# @date 2018-08-18
18
19
"""
20
This script plots fcd data for each vehicle using either
21
- distance vs speed
22
- time vs speed
23
- time vs distance
24
25
Individual trajectories can be clicked in interactive mode to print the vehicle Id on the console
26
"""
27
from __future__ import absolute_import
28
from __future__ import print_function
29
import os
30
import sys
31
from collections import defaultdict
32
import math
33
import matplotlib.pyplot as plt
34
35
from sumolib.xml import parse_fast_nested # noqa
36
from sumolib.miscutils import uMin, uMax, parseTime # noqa
37
from sumolib.options import ArgumentParser # noqa
38
from sumolib.visualization import helpers # noqa
39
40
KEYS = {
41
't': 'Time',
42
's': 'Speed',
43
'd': 'Distance',
44
'a': 'Acceleration',
45
'i': 'Angle',
46
'x': 'x-Position',
47
'y': 'y-Position',
48
'k': 'kilometrage',
49
'g': 'gap',
50
}
51
52
53
def getOptions(args=None):
54
optParser = ArgumentParser(conflict_handler="resolve")
55
helpers.addInteractionOptions(optParser)
56
helpers.addPlotOptions(optParser)
57
optParser.add_option("-t", "--trajectory-type", category="processing", dest="ttype", default="ds",
58
help="select two letters from [t, s, d, a, i, x, y, k, g] to plot"
59
+ " Time, Speed, Distance, Acceleration, Angle,"
60
+ " x-Position, y-Position, Kilometrage, leaderGap."
61
+ " Default 'ds' plots Distance vs. Speed")
62
optParser.add_option("--persons", category="processing", action="store_true",
63
default=False, help="plot person trajectories")
64
optParser.add_option("--meso", category="processing", action="store_true",
65
default=False, help="plot meso trajectories")
66
optParser.add_option("-s", "--show", category="processing", action="store_true",
67
default=False, help="show plot directly")
68
optParser.add_option("--csv-output", category="output", dest="csv_output", help="write plot as csv", metavar="FILE")
69
optParser.add_option("-b", "--ballistic", category="processing", action="store_true", default=False,
70
help="perform ballistic integration of distance")
71
optParser.add_option("--filter-route", category="processing", dest="filterRoute",
72
help="only export trajectories that pass the given list of edges (regardless of gaps)")
73
optParser.add_option("--filter-edges", category="processing", dest="filterEdges",
74
help="only consider data for the given list of edges")
75
optParser.add_option("--filter-ids", category="processing", dest="filterIDs",
76
help="only consider data for the given list of vehicle (or person) ids")
77
optParser.add_option("-p", "--pick-distance", category="processing", dest="pickDist", type=float, default=1,
78
help="pick lines within the given distance in interactive plot mode")
79
optParser.add_option("-i", "--invert-distance-angle", category="processing", dest="invertDistanceAngle", type=float,
80
help="invert distance for trajectories with a average angle near FLOAT")
81
optParser.add_option("--label", category="processing", help="plot label (default input file name")
82
optParser.add_option("--invert-yaxis", category="processing", dest="invertYAxis", action="store_true",
83
default=False, help="Invert the Y-Axis")
84
optParser.add_option("--legend", category="processing", action="store_true", default=False, help="Add legend")
85
optParser.add_option("-v", "--verbose", category="processing", action="store_true",
86
default=False, help="tell me what you are doing")
87
optParser.add_option("fcdfiles", nargs="+", category="input", type=ArgumentParser.file, help="FCD input file(s)")
88
89
options, args = optParser.parse_known_args(args=args)
90
91
# keep old presets from before integration of common options
92
options.nolegend = not options.legend
93
if options.show:
94
sys.stderr.write("Option --show is now set by default and will be removed in the future." +
95
"Use --blind to disable the plot window\n")
96
97
if options.filterRoute is not None:
98
options.filterRoute = options.filterRoute.split(',')
99
if options.filterEdges is not None:
100
options.filterEdges = set(options.filterEdges.split(','))
101
if options.filterIDs is not None:
102
options.filterIDs = set(options.filterIDs.split(','))
103
return options
104
105
106
def write_csv(data, fname):
107
with open(fname, 'w') as f:
108
for veh, vals in sorted(data.items()):
109
f.write('"%s"\n' % veh)
110
for x in zip(*[vals[k] for k in KEYS if k in vals]):
111
f.write(" ".join(map(str, x)) + "\n")
112
f.write('\n')
113
114
115
def short_names(filenames):
116
if len(filenames) == 1:
117
return filenames
118
reversedNames = [''.join(reversed(f)) for f in filenames]
119
prefixLen = len(os.path.commonprefix(filenames))
120
suffixLen = len(os.path.commonprefix(reversedNames))
121
return [f[prefixLen:-suffixLen] for f in filenames]
122
123
124
def onpick(event):
125
mevent = event.mouseevent
126
print("veh=%s x=%d y=%d" % (event.label, mevent.xdata, mevent.ydata))
127
128
129
def main(options):
130
fig = plt.figure(figsize=(14, 9), dpi=100)
131
fig.canvas.mpl_connect('pick_event', onpick)
132
133
xdata = None
134
ydata = None
135
shortFileNames = short_names(options.fcdfiles)
136
xdata = options.ttype[0]
137
ydata = options.ttype[1]
138
if (len(options.ttype) == 2
139
and xdata in KEYS
140
and ydata in KEYS):
141
xLabel = KEYS[xdata]
142
yLabel = KEYS[ydata]
143
plt.xlabel(xLabel)
144
plt.ylabel(yLabel)
145
plt.title(','.join(shortFileNames) if options.label is None else options.label)
146
else:
147
sys.exit("unsupported plot type '%s'" % options.ttype)
148
149
element = 'vehicle'
150
location = 'lane'
151
if options.persons:
152
element = 'person'
153
location = 'edge'
154
elif options.meso:
155
location = 'edge'
156
157
routes = defaultdict(list) # vehID -> recorded edges
158
# vehID -> (times, speeds, distances, accelerations, angles, xPositions, yPositions, kilometrage)
159
attrs = ['id', 'x', 'y', 'angle', 'speed', location]
160
if 'k' in options.ttype:
161
attrs.append('distance')
162
if 'g' in options.ttype:
163
attrs.append('leaderGap')
164
data = defaultdict(lambda: defaultdict(list))
165
for fileIndex, fcdfile in enumerate(options.fcdfiles):
166
totalVehs = 0
167
filteredVehs = 0
168
for timestep, vehicle in parse_fast_nested(fcdfile, 'timestep', ['time'],
169
element, attrs):
170
totalVehs += 1
171
vehID = vehicle.id
172
if options.filterIDs and vehID not in options.filterIDs:
173
continue
174
if len(options.fcdfiles) > 1:
175
suffix = shortFileNames[fileIndex]
176
if len(suffix) > 0:
177
vehID += "#" + suffix
178
if options.persons or options.meso:
179
edge = vehicle.edge
180
else:
181
edge = vehicle.lane[0:vehicle.lane.rfind('_')]
182
if len(routes[vehID]) == 0 or routes[vehID][-1] != edge:
183
routes[vehID].append(edge)
184
if options.filterEdges and edge not in options.filterEdges:
185
continue
186
time = parseTime(timestep.time)
187
speed = float(vehicle.speed)
188
prevTime = time
189
prevSpeed = speed
190
prevDist = 0
191
if vehID in data:
192
prevTime = data[vehID]['t'][-1]
193
prevSpeed = data[vehID]['s'][-1]
194
prevDist = data[vehID]['d'][-1]
195
data[vehID]['t'].append(time)
196
data[vehID]['s'].append(speed)
197
data[vehID]['i'].append(float(vehicle.angle))
198
data[vehID]['x'].append(float(vehicle.x))
199
data[vehID]['y'].append(float(vehicle.y))
200
if 'k' in options.ttype:
201
data[vehID]['k'].append(float(vehicle.distance))
202
if 'g' in options.ttype:
203
data[vehID]['g'].append(float(vehicle.leaderGap))
204
if prevTime == time:
205
data[vehID]['a'].append(0)
206
else:
207
data[vehID]['a'].append((speed - prevSpeed) / (time - prevTime))
208
209
if options.ballistic:
210
avgSpeed = (speed + prevSpeed) / 2
211
else:
212
avgSpeed = speed
213
data[vehID]['d'].append(prevDist + (time - prevTime) * avgSpeed)
214
filteredVehs += 1
215
if totalVehs == 0 or filteredVehs == 0 or options.verbose:
216
print("Found %s datapoints in %s and kept %s" % (
217
totalVehs, fcdfile, filteredVehs))
218
219
if filteredVehs == 0:
220
sys.exit()
221
222
def line_picker(line, mouseevent):
223
if mouseevent.xdata is None:
224
return False, dict()
225
# minxy = None
226
# mindist = 10000
227
for x, y in zip(line.get_xdata(), line.get_ydata()):
228
dist = math.sqrt((x - mouseevent.xdata) ** 2 + (y - mouseevent.ydata) ** 2)
229
if dist < options.pickDist:
230
return True, dict(label=line.get_label())
231
# else:
232
# if dist < mindist:
233
# print(" ", x,y, dist, (x - mouseevent.xdata) ** 2, (y - mouseevent.ydata) ** 2)
234
# mindist = dist
235
# minxy = (x, y)
236
# print(mouseevent.xdata, mouseevent.ydata, minxy, dist,
237
# line.get_label())
238
return False, dict()
239
240
minY = uMax
241
maxY = uMin
242
minX = uMax
243
maxX = uMin
244
245
addArgs = {"picker": line_picker, "linestyle": options.linestyle}
246
if options.marker is not None:
247
addArgs["marker"] = options.marker
248
249
for vehID, d in data.items():
250
if options.filterRoute is not None:
251
skip = False
252
route = routes[vehID]
253
for required in options.filterRoute:
254
if required not in route:
255
skip = True
256
break
257
if skip:
258
continue
259
if options.invertDistanceAngle is not None:
260
avgAngle = sum(d[4]) / len(d[4])
261
if abs(avgAngle - options.invertDistanceAngle) < 45:
262
maxDist = d[2][-1]
263
for i, v in enumerate(d[2]):
264
d[2][i] = maxDist - v
265
266
minY = min(minY, min(d[ydata]))
267
maxY = max(maxY, max(d[ydata]))
268
minX = min(minX, min(d[xdata]))
269
maxX = max(maxX, max(d[xdata]))
270
271
plt.plot(d[xdata], d[ydata], label=vehID, **addArgs)
272
if options.invertYAxis:
273
plt.axis([minX, maxX, maxY, minY])
274
if options.csv_output is not None:
275
write_csv(data, options.csv_output)
276
helpers.closeFigure(fig, fig.axes[0], options)
277
278
279
if __name__ == "__main__":
280
main(getOptions())
281
282