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