Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/output/scheduleStats.py
169674 views
1
#!/usr/bin/env python
2
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3
# Copyright (C) 2012-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 scheduleStats.py
15
# @author Jakob Erdmann
16
# @date 2021-04-20
17
18
"""
19
Compare arrival and departur at stops between an input schedule (route-file) and
20
simulation output (stop-output).
21
"""
22
23
from __future__ import absolute_import
24
from __future__ import print_function
25
26
import os
27
import sys
28
from math import isnan
29
30
import pandas as pd
31
32
if 'SUMO_HOME' in os.environ:
33
sys.path.append(os.path.join(os.environ['SUMO_HOME'], 'tools'))
34
import sumolib # noqa
35
from sumolib.miscutils import parseTime # noqa
36
from sumolib.statistics import Statistics # noqa
37
from sumolib.xml import parse # noqa
38
39
pd.options.display.width = 0 # auto-detect terminal width
40
41
STATS = {
42
# selector -> description, function
43
'd': ('depart delay', lambda r, s: s.add(r['sim_ended'] - r['until'], key(r))),
44
'a': ('arrival delay', lambda r, s: s.add(r['sim_started'] - r['arrival'], key(r))),
45
'de': ('depart delay',
46
lambda r, s: s.add(r['sim_ended'] - (r['until'] if isnan(r['ended']) else r['ended']), key(r))),
47
'as': ('arrival delay',
48
lambda r, s: s.add(r['sim_started'] - (r['arrival'] if isnan(r['started']) else r['started']), key(r))),
49
's': ('stop delay', lambda r, s: s.add(r['until'] - r['arrival'] - (r['sim_ended'] - r['sim_started']), key(r))),
50
't': ('traveltime schedule delta', lambda r, s: s.add(r['traveltime'] - (r['sim_traveltime'])
51
if not isnan(r['traveltime']) and not isnan(r['sim_traveltime']) else 0, key(r))),
52
'T': ('traveltime recording delta', lambda r, s: s.add(r['traveltime_actual'] - (r['sim_traveltime'])
53
if not isnan(r['traveltime_actual']) and not isnan(r['sim_traveltime']) else 0, key(r))),
54
}
55
56
GROUPSTATS = {
57
'mean': lambda s: s.avg(),
58
'median': lambda s: s.median(),
59
'min': lambda s: s.min,
60
'max': lambda s: s.max,
61
}
62
63
64
def get_options(args=None):
65
parser = sumolib.options.ArgumentParser(description="Compare route-file stop timing with stop-output")
66
parser.add_argument("-r", "--route-file", dest="routeFile",
67
help="Input route file")
68
parser.add_argument("-s", "--stop-file", dest="stopFile",
69
help="Input stop-output file")
70
parser.add_argument("-o", "--xml-output", dest="output",
71
help="xml output file")
72
parser.add_argument("-t", "--statistic-type", default="d", dest="sType",
73
help="Code for statistic type from %s" % STATS.keys())
74
parser.add_argument("-g", "--group-by", dest="groupBy",
75
help="Code for grouping results")
76
parser.add_argument("-i", "--histogram", dest="histogram", type=float,
77
help="histogram bin size")
78
parser.add_argument("-I", "--group-histogram", dest="gHistogram", type=float,
79
help="group histogram bin size")
80
parser.add_argument("-T", "--group-statistic-type", dest="gType", default="mean",
81
help="attribute for group statistic from %s" % GROUPSTATS.keys())
82
parser.add_argument("-p", "--precision", default=1, type=int,
83
help="output precision")
84
parser.add_argument("-H", "--human-readable-time", dest="hrTime", action="store_true", default=False,
85
help="Write time values as hour:minute:second or day:hour:minute:second rathern than seconds")
86
parser.add_argument("-v", "--verbose", action="store_true",
87
default=False, help="tell me what you are doing")
88
89
options = parser.parse_args(args=args)
90
if options.routeFile is None or options.stopFile is None:
91
parser.print_help()
92
sys.exit()
93
94
if options.sType not in STATS:
95
parser.print_help()
96
sys.exit()
97
98
if options.groupBy:
99
options.groupBy = options.groupBy.split(',')
100
101
return options
102
103
104
ATTR_CONVERSIONS = {
105
'arrival': parseTime,
106
'until': parseTime,
107
'started': parseTime,
108
'ended': parseTime,
109
}
110
111
112
def getStopID(stop):
113
if stop.hasAttribute("busStop"):
114
return stop.busStop
115
else:
116
# stopinfo has no endPos, only pos
117
# return "%s,%s" % (stop.lane, stop.endPos)
118
return "%s" % stop.lane
119
120
121
def key(row):
122
return "%s_%s" % (row['tripId'], row['stopID'])
123
124
125
def main(options):
126
nan = float("nan")
127
128
columns = [
129
'vehID',
130
'tripId', # tripId of current stop or set by earlier stop
131
'stopID', # busStop id or lane,pos
132
'priorStop', # busStop id or lane,pos
133
'arrival', # route-input
134
'until', # route-input
135
'started', # route-input
136
'ended', # route-input
137
'traveltime', # route-input (as scheduled)
138
'traveltime_actual', # route-input
139
]
140
141
columns2 = columns[:3] + [
142
'sim_started', # stop-output
143
'sim_ended', # stop-input
144
'sim_traveltime', # stop-input
145
]
146
147
stops = []
148
tripIds = dict() # vehID -> lastTripId
149
priorStops = dict() # vehID -> lastStopID
150
priorUntil = dict() # vehID -> lastStopUntil
151
priorEnded = dict() # vehID -> lastStopEnded
152
for vehicle in parse(options.routeFile, ['vehicle', 'trip'],
153
heterogeneous=True, attr_conversions=ATTR_CONVERSIONS):
154
if vehicle.stop is not None:
155
for stop in vehicle.stop:
156
vehID = vehicle.id
157
tripId = stop.getAttributeSecure("tripId", tripIds.get(vehID))
158
tripIds[vehID] = tripId
159
stopID = getStopID(stop)
160
priorStop = priorStops.get(vehID)
161
arrival = stop.getAttributeSecure("arrival", nan)
162
until = stop.getAttributeSecure("until", nan)
163
started = stop.getAttributeSecure("started", nan)
164
ended = stop.getAttributeSecure("ended", nan)
165
traveltime = nan
166
if not isnan(priorUntil.get(vehID, nan)) and not isnan(arrival):
167
traveltime = arrival - priorUntil[vehID]
168
traveltime_actual = nan
169
if not isnan(priorEnded.get(vehID, nan)) and not isnan(started):
170
traveltime_actual = started - priorEnded[vehID]
171
172
priorStops[vehID] = stopID
173
priorUntil[vehID] = until
174
priorEnded[vehID] = ended
175
176
stops.append((vehID, tripId, stopID, priorStop, arrival, until,
177
started, ended, traveltime, traveltime_actual))
178
179
print("Parsed %s stops" % len(stops))
180
181
simStops = []
182
tripIds = dict() # vehID -> lastTripId
183
priorStops = dict() # vehID -> lastStopID
184
priorEnded = dict() # vehID -> lastStopUntil
185
for stop in parse(options.stopFile, "stopinfo", heterogeneous=True,
186
attr_conversions=ATTR_CONVERSIONS):
187
vehID = stop.id
188
tripId = stop.getAttributeSecure("tripId", tripIds.get(vehID))
189
tripIds[vehID] = tripId
190
stopID = getStopID(stop)
191
priorStop = priorStops.get(vehID)
192
started = stop.getAttributeSecure("started", nan)
193
ended = stop.getAttributeSecure("ended", nan)
194
sim_traveltime = nan
195
if not isnan(priorEnded.get(vehID, nan)) and not isnan(started):
196
sim_traveltime = started - priorEnded[vehID]
197
priorStops[vehID] = stopID
198
priorEnded[vehID] = ended
199
200
simStops.append((vehID, tripId, stopID, # priorStop,
201
started, ended, sim_traveltime))
202
203
print("Parsed %s stopinfos" % len(simStops))
204
205
dfSchedule = pd.DataFrame.from_records(stops, columns=columns)
206
dfSim = pd.DataFrame.from_records(simStops, columns=columns2)
207
# merge on common columns vehID, tripId, stopID
208
df = pd.merge(dfSchedule, dfSim,
209
on=columns[:3],
210
# how="outer",
211
how="inner",
212
)
213
214
print("Found %s matches" % len(df))
215
216
if options.verbose:
217
# print(dfSchedule)
218
# print(dfSim)
219
print(df)
220
221
if options.output:
222
outf = open(options.output, 'w')
223
sumolib.writeXMLHeader(outf, root="scheduleStats")
224
225
description, fun = STATS[options.sType]
226
useHist = options.histogram is not None
227
useGHist = options.gHistogram is not None
228
if options.groupBy:
229
numGroups = 0
230
stats = []
231
gs = Statistics("%s %s grouped by [%s]" % (options.gType, description, ','.join(options.groupBy)),
232
abs=True, histogram=useGHist, scale=options.gHistogram)
233
for name, group in df.groupby(options.groupBy if len(options.groupBy) > 1 else options.groupBy[0]):
234
numGroups += 1
235
s = Statistics("%s:%s" % (description, name), abs=True, histogram=useHist, scale=options.histogram)
236
group.apply(fun, axis=1, args=(s,))
237
gVal = GROUPSTATS[options.gType](s)
238
gs.add(gVal, name)
239
stats.append((gVal, s))
240
241
stats.sort(key=lambda x: x[0])
242
for gVal, s in stats:
243
print(s.toString(precision=options.precision, histStyle=2))
244
if options.output:
245
outf.write(s.toXML(precision=options.precision))
246
print()
247
print(gs.toString(precision=options.precision, histStyle=2))
248
if options.output:
249
outf.write(gs.toXML(precision=options.precision))
250
251
else:
252
s = Statistics(description, abs=True, histogram=useHist, scale=options.histogram)
253
df.apply(fun, axis=1, args=(s,))
254
print(s.toString(precision=options.precision, histStyle=2))
255
if options.output:
256
outf.write(s.toXML(precision=options.precision))
257
258
if options.output:
259
outf.write("</scheduleStats>\n")
260
outf.close()
261
262
263
if __name__ == "__main__":
264
main(get_options())
265
266