Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/routeSampler.py
169659 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 routeSampler.py
15
# @author Jakob Erdmann
16
# @date 2020-02-07
17
18
"""
19
Samples routes from a given set to fulfill specified counting data (edge counts or turn counts)
20
"""
21
22
from __future__ import absolute_import
23
from __future__ import print_function
24
25
import os
26
import sys
27
from collections import defaultdict
28
from math import ceil, modf
29
# multiprocessing imports
30
import multiprocessing
31
import numpy as np
32
33
try:
34
from StringIO import StringIO
35
except ImportError:
36
from io import StringIO
37
38
if 'SUMO_HOME' in os.environ:
39
sys.path.append(os.path.join(os.environ['SUMO_HOME'], 'tools'))
40
import sumolib # noqa
41
from sumolib.miscutils import parseTime, humanReadableTime, Benchmarker # noqa
42
from sumolib.statistics import setPrecision # noqa
43
44
PRESERVE_INPUT_COUNT = 'input'
45
46
47
def get_options(args=None):
48
op = sumolib.options.ArgumentParser(description="Sample routes to match counts")
49
# input
50
op.add_argument("-r", "--route-files", category="input", dest="routeFiles", type=op.route_file_list,
51
help="Input route file", required=True)
52
op.add_argument("-t", "--turn-files", category="input", dest="turnFiles", type=op.file_list,
53
help="Input turn-count file")
54
op.add_argument("-T", "--turn-ratio-files", category="input", dest="turnRatioFiles", type=op.file_list,
55
help="Input turn-ratio file")
56
op.add_argument("-d", "--edgedata-files", category="input", dest="edgeDataFiles", type=op.file_list,
57
help="Input edgeData file (for counts)")
58
op.add_argument("-O", "--od-files", category="input", dest="odFiles", type=op.file_list,
59
help="Input edgeRelation and tazRelation files for origin-destination counts")
60
op.add_argument("--taz-files", category="input", dest="tazFiles", type=op.file_list,
61
help="Input TAZ (district) definitions for interpreting tazRelation files")
62
op.add_argument("--edgedata-attribute", category="input", dest="edgeDataAttr", default="entered",
63
help="Read edgeData counts from the given attribute")
64
op.add_argument("--arrival-attribute", category="input", dest="arrivalAttr",
65
help="Read arrival counts from the given edgeData file attribute")
66
op.add_argument("--depart-attribute", category="input", dest="departAttr",
67
help="Read departure counts from the given edgeData file attribute")
68
op.add_argument("--turn-attribute", category="input", dest="turnAttr", default="count",
69
help="Read turning counts and origin-destination counts from the given attribute")
70
op.add_argument("--turn-ratio-attribute", category="input", dest="turnRatioAttr", default="probability",
71
help="Read turning ratios from the given attribute")
72
# output
73
op.add_argument("-o", "--output-file", category="output", dest="out", default="out.rou.xml", type=op.route_file,
74
help="Output route file")
75
op.add_argument("--mismatch-output", category="output", dest="mismatchOut",
76
help="write cout-data with overflow/underflow information to FILE")
77
op.add_argument("--precision", category="output", type=int, dest="precision", default=2,
78
help="Number of decimal digits in output")
79
op.add_argument("--keep-stops", category="output", dest="keepStops", action="store_true", default=False,
80
help="Preserve stops from the input routes")
81
op.add_argument("-v", "--verbose", category="output", action="store_true", default=False,
82
help="tell me what you are doing")
83
op.add_argument("-V", "--verbose.histograms", category="output", dest="verboseHistogram", action="store_true",
84
default=False, help="print histograms of edge numbers and detector passing count")
85
op.add_argument("--verbose.timing", category="output", dest="verboseTiming", action="store_true",
86
default=False, help="print time performance information")
87
# attributes
88
op.add_argument("--prefix", category="attributes", dest="prefix", default="",
89
help="prefix for the vehicle ids")
90
op.add_argument("-a", "--attributes", category="attributes", dest="vehattrs", default="",
91
help="additional vehicle attributes")
92
op.add_argument("-f", "--write-flows", category="attributes", dest="writeFlows",
93
help="write flows with the give style instead of vehicles [number|probability|poisson]")
94
op.add_argument("-I", "--write-route-ids", category="attributes", dest="writeRouteIDs", action="store_true",
95
default=False, help="write routes with ids")
96
op.add_argument("-u", "--write-route-distribution", category="attributes", dest="writeRouteDist",
97
help="write routeDistribution with the given ID instead of individual routes")
98
op.add_argument("--pedestrians", category="attributes", action="store_true", default=False,
99
help="write person walks instead of vehicle routes")
100
# time
101
op.add_argument("-b", "--begin", category="time",
102
help="custom begin time (seconds or H:M:S)")
103
op.add_argument("-e", "--end", category="time",
104
help="custom end time (seconds or H:M:S)")
105
op.add_argument("-i", "--interval", category="time",
106
help="custom aggregation interval (seconds or H:M:S)")
107
op.add_argument("--depart-distribution", category="time", dest="departDistVals",
108
help="load list of densities that cover [begin, end] to customize departure time probabilities")
109
# processing
110
op.add_argument("--turn-max-gap", type=int, dest="turnMaxGap", default=0,
111
help="Allow at most a gap of INT edges between from-edge and to-edge")
112
op.add_argument("--total-count", dest="totalCount",
113
help="Set a total count that should be reached (either as single value that is split " +
114
" proportionally among all intervals or as a list of counts per interval)." +
115
" Setting the value 'input' preserves input vehicle counts in each interval.")
116
op.add_argument("--extra-od", dest="extraOD", action="store_true", default=False,
117
help="Permit traffic between OD-pairs that did not occur in the input")
118
op.add_argument("-s", "--seed", type=int, default=42,
119
help="random seed")
120
op.add_argument("--weighted", dest="weighted", action="store_true", default=False,
121
help="Sample routes according to their probability (or count)")
122
op.add_argument("--optimize",
123
help="set optimization method level (full, INT boundary)")
124
op.add_argument("--optimize-input", dest="optimizeInput", action="store_true", default=False,
125
help="Skip resampling and run optimize directly on the input routes")
126
op.add_argument("--init-input", dest="initInput", action="store_true", default=False,
127
help="use loaded routes as initialization for the used routes")
128
op.add_argument("--init-input.remove-overflow", dest="initInputRemove", action="store_true", default=False,
129
help="use loaded routes as initialization but remove those that are responsible for overflow")
130
op.add_argument("--no-sampling", dest="noSampling", action="store_true", default=False,
131
help="Skip sampling of routes")
132
op.add_argument("--min-count", dest="minCount", type=int, default=1,
133
help="Set minimum number of counting locations that a route must visit")
134
op.add_argument("--minimize-vehicles", dest="minimizeVehs", type=float, default=0,
135
help="Set optimization factor from [0, 1[ for reducing the number of vehicles" +
136
"(prefer routes that pass multiple counting locations over routes that pass fewer)")
137
op.add_argument("--geh-ok", dest="gehOk", type=float, default=5,
138
help="threshold for acceptable GEH values")
139
op.add_argument("--geh-scale", dest="gehScale", type=float, default=None,
140
help="Should be set to 0.1 when loading traffic for a full day "
141
"(estimating peak hour traffic as 1/10 of daily traffic)")
142
op.add_argument("--turn-ratio-total", dest="turnRatioTotal", type=float, default=1,
143
help="Set value for normalizing turning ratios (default 1)")
144
op.add_argument("--turn-ratio-tolerance", dest="turnRatioTolerance", type=float,
145
help="Set tolerance for error in resulting ratios (relative to turn-ratio-total)")
146
op.add_argument("--turn-ratio-abs-tolerance", dest="turnRatioAbsTolerance", type=int,
147
default=1, help="Set tolerance for error in resulting turning ratios as absolute count")
148
op.add_argument("--threads", dest="threads", type=int, default=1,
149
help="If parallelization is desired, enter the number of CPUs to use. Set to a value >> then " +
150
"your machines CPUs if you want to utilize all CPUs (Default is 1)")
151
152
options = op.parse_args(args=args)
153
if (options.routeFiles is None or
154
(options.turnFiles is None
155
and options.turnRatioFiles is None
156
and options.edgeDataFiles is None
157
and options.odFiles is None)):
158
sys.stderr.write("At least one file with counting data must be loaded\n")
159
sys.exit()
160
if options.writeRouteIDs and options.writeRouteDist:
161
sys.stderr.write("Only one of the options --write-route-ids and --write-route-distribution may be used\n")
162
sys.exit()
163
if options.writeFlows not in [None, "number", "probability", "poisson"]:
164
sys.stderr.write("Options --write-flows only accepts arguments 'number', 'probability' and 'poisson'\n")
165
sys.exit()
166
167
for attr in ["edgeDataAttr", "arrivalAttr", "departAttr", "turnAttr", "turnRatioAttr"]:
168
if getattr(options, attr) not in [None, "None"]:
169
setattr(options, attr, getattr(options, attr).split(","))
170
171
options.routeFiles = options.routeFiles.split(',')
172
options.turnFiles = options.turnFiles.split(',') if options.turnFiles is not None else []
173
options.turnRatioFiles = options.turnRatioFiles.split(',') if options.turnRatioFiles is not None else []
174
options.edgeDataFiles = options.edgeDataFiles.split(',') if options.edgeDataFiles is not None else []
175
options.odFiles = options.odFiles.split(',') if options.odFiles is not None else []
176
options.tazFiles = options.tazFiles.split(',') if options.tazFiles is not None else []
177
if options.vehattrs and options.vehattrs[0] != ' ':
178
options.vehattrs = ' ' + options.vehattrs
179
180
if options.optimize is not None:
181
try:
182
import scipy.optimize # noqa
183
if options.optimize != "full":
184
try:
185
options.optimize = int(options.optimize)
186
except Exception:
187
print("Option optimize requires the value 'full' or an integer", file=sys.stderr)
188
sys.exit(1)
189
except ImportError:
190
print("Cannot use optimization (scipy not installed)", file=sys.stderr)
191
sys.exit(1)
192
193
if options.optimizeInput:
194
if not isinstance(options.optimize, int):
195
print("Option --optimize-input requires an integer argument for --optimize", file=sys.stderr)
196
sys.exit(1)
197
options.initInput = True
198
options.noSampling = True
199
200
if options.initInputRemove:
201
options.initInput = True
202
203
if options.threads > 1 and sys.version_info[0] < 3:
204
print("Using multiple cpus is only supported for python 3", file=sys.stderr)
205
sys.exit(1)
206
207
if options.totalCount:
208
if options.totalCount != PRESERVE_INPUT_COUNT:
209
options.totalCount = list(map(int, options.totalCount.split(',')))
210
211
if options.departDistVals:
212
sep = ',' if ',' in options.departDistVals else None
213
options.departDistVals = list(map(float, options.departDistVals.split(sep)))
214
215
return options
216
217
218
class Routes:
219
def __init__(self, routefiles, keepStops, rng):
220
self.rng = rng
221
self.all = []
222
self.edgeProbs = defaultdict(zero)
223
self.edgeIDs = {}
224
self.withProb = 0
225
self.routeStops = defaultdict(list) # list of list of stops for the given edges
226
for routefile in routefiles:
227
warned = False
228
# not all routes may have specified probability, in this case use their number of occurrences
229
for r in sumolib.xml.parse(routefile, ['route', 'walk'], heterogeneous=True):
230
if r.edges is None:
231
if not warned:
232
print("Warning: Ignoring walk in file '%s' because it does not contain edges." % routefile,
233
file=sys.stderr)
234
warned = True
235
continue
236
edges = tuple(r.edges.split())
237
self.all.append(edges)
238
prob = float(r.getAttributeSecure("probability", 1))
239
if r.hasAttribute("probability"):
240
self.withProb += 1
241
prob = float(r.probability)
242
else:
243
prob = 1
244
if prob <= 0:
245
print("Warning: route probability must be positive (edges=%s)" % r.edges, file=sys.stderr)
246
prob = 0
247
if r.hasAttribute("id"):
248
self.edgeIDs[edges] = r.id
249
self.edgeProbs[edges] += prob
250
if keepStops and r.stop:
251
self.routeStops[edges].append(list(r.stop))
252
253
self.unique = sorted(list(self.edgeProbs.keys()))
254
self.uniqueSets = [set(edges) for edges in self.unique]
255
self.number = len(self.unique)
256
self.edges2index = dict([(e, i) for i, e in enumerate(self.unique)])
257
if len(self.unique) == 0:
258
print("Error: no input routes loaded", file=sys.stderr)
259
sys.exit()
260
self.probabilities = np.array([self.edgeProbs[e] for e in self.unique], dtype=np.float64)
261
262
def write(self, outf, prefix, intervalPrefix, routeIndex, count, writeDist=False):
263
edges = self.unique[routeIndex]
264
indent = ' ' * 8
265
comment = []
266
probability = ""
267
ID = ' id="%s%s%s"' % (prefix, intervalPrefix, routeIndex) if prefix is not None else ""
268
if ID:
269
probability = ' probability="%s"' % count
270
if not writeDist:
271
indent = ' ' * 4
272
if ID and edges in self.edgeIDs:
273
comment.append("(%s)" % self.edgeIDs[edges])
274
comment = ' '.join(comment)
275
if comment:
276
comment = " <!-- %s -->" % comment
277
278
stops = []
279
stopCandidates = self.routeStops.get(edges)
280
if stopCandidates:
281
stops = stopCandidates[self.rng.choice(range(len(stopCandidates)))]
282
close = '' if stops else '/'
283
outf.write('%s<route%s edges="%s"%s%s>%s\n' % (indent, ID, ' '.join(edges), probability, close, comment))
284
if stops:
285
for stop in stops:
286
outf.write(stop.toXML(indent + ' ' * 4))
287
outf.write('%s</route>\n' % indent)
288
289
290
class DepartDist:
291
def __init__(self, vals, begin, end):
292
self.begin = begin
293
self.end = end
294
# normalize vals
295
s = sum(vals)
296
vals = [v / s for v in vals]
297
# prepare CDF
298
binWidth = (end - begin) / len(vals)
299
self.cdf_x = [begin + i * binWidth for i in range(len(vals) + 1)]
300
self.cdf_y = [0]
301
for v in vals:
302
self.cdf_y.append(self.cdf_y[-1] + v)
303
304
def sample(self, rng, n, begin, end):
305
"""sample n values between begin and end"""
306
left, right = np.interp([begin, end], self.cdf_x, self.cdf_y)
307
# construct inverse CDF truncated to left and right
308
icdf_x = [left]
309
icdf_y = [begin]
310
for x, y in zip(self.cdf_y, self.cdf_x):
311
if y > begin and y < end:
312
icdf_x.append(x)
313
icdf_y.append(y)
314
icdf_x.append(right)
315
icdf_y.append(end)
316
317
# obtain n random variables between left and right
318
scale = right - left
319
r = [left + v * scale for v in rng.random(n)]
320
321
# evaluate icdf
322
return np.interp(r, icdf_x, icdf_y)
323
324
325
class CountData:
326
def __init__(self, count, edgeTuple, allRoutes, isOrigin, isDest, isRatio, isTaz, options):
327
self.index = None
328
self.origCount = count
329
self.count = count
330
self.edgeTuple = edgeTuple
331
self.isOrigin = isOrigin
332
self.isDest = isDest
333
self.isRatio = isRatio
334
self.isTaz = isTaz
335
self.ratioSiblings = []
336
self.assignedCount = 0
337
self.options = options # multiprocessing had issue with sumolib.options.getOptions().turnMaxGap
338
self.routeSet = set()
339
for routeIndex, edges in enumerate(allRoutes.unique):
340
if self.routePasses(edges, allRoutes.uniqueSets[routeIndex]) is not None:
341
self.routeSet.add(routeIndex)
342
343
def routePasses(self, edges, edgeSet):
344
if self.isTaz:
345
if (inTaz(self.options, edges[0], self.edgeTuple[0], True) and
346
inTaz(self.options, edges[-1], self.edgeTuple[-1], False)):
347
return 0
348
else:
349
return None
350
if self.isOrigin or self.isDest:
351
passes = ((not self.isOrigin or edges[0] == self.edgeTuple[0]) and
352
(not self.isDest or edges[-1] == self.edgeTuple[-1]))
353
if not passes:
354
return None
355
else:
356
return 0 if self.isOrigin else len(edges) - 1
357
firstEdge = self.edgeTuple[0]
358
if firstEdge in edgeSet:
359
i = edges.index(firstEdge)
360
maxDelta = self.options.turnMaxGap + 1
361
for edge in self.edgeTuple[1:]:
362
if edge in edgeSet:
363
try:
364
i2 = edges.index(edge, i)
365
if i2 - i > maxDelta:
366
return None
367
i = i2
368
except ValueError:
369
# other edge came earlier in route
370
return None
371
else:
372
# other edge not in route
373
return None
374
else:
375
# first edge not in route
376
return None
377
return i
378
379
def use(self, n=1):
380
self.count -= n
381
self.assignedCount += n
382
383
def addCount(self, count):
384
self.count += count
385
self.origCount += count
386
387
def updateTurnRatioCounts(self, openRoutes, openCounts, updateSiblings=False):
388
if self.isRatio:
389
total = self.getSiblingCount()
390
permitted = total * self.origCount + self.options.turnRatioAbsTolerance
391
if permitted > self.assignedCount:
392
self.count = permitted - self.assignedCount
393
for rI in self.routeSet:
394
if rI not in openRoutes:
395
openRoutes.append(rI)
396
if self.index not in openCounts:
397
openCounts.append(self.index)
398
if updateSiblings:
399
for cd2 in self.ratioSiblings:
400
if cd2 != self:
401
cd2.updateTurnRatioCounts(openRoutes, openCounts)
402
403
def getSiblingCount(self):
404
if not self.isRatio:
405
return None
406
return sum([cd2.assignedCount for cd2 in self.ratioSiblings])
407
408
def assignedProbability(self):
409
if not self.isRatio:
410
return None
411
sibCount = self.getSiblingCount()
412
return 0 if sibCount == 0 else self.assignedCount / sibCount
413
414
def __repr__(self):
415
return "CountData(edges=%s, count=%s, origCount=%s%s%s%s%s)\n" % (
416
self.edgeTuple, self.count, self.origCount,
417
", isOrigin=True" if self.isOrigin else "",
418
", isDest=True" if self.isDest else "",
419
", isRatio=True" if self.isRatio else "",
420
(", sibs=%s" % len(self.ratioSiblings)) if self.isRatio else "")
421
422
423
def _run_func(args):
424
func, interval, kwargs, num = args
425
kwargs["cpuIndex"] = num
426
return num, func(interval=interval, **kwargs)
427
428
429
def multi_process(cpu_num, interval_list, func, outf, mismatchf, **kwargs):
430
cpu_count = min(cpu_num, multiprocessing.cpu_count()-1, len(interval_list))
431
interval_split = np.array_split(interval_list, cpu_count)
432
# pool = multiprocessing.Pool(processes=cpu_count)
433
with multiprocessing.get_context("spawn").Pool() as pool:
434
results = pool.map(_run_func, [(func, interval, kwargs, i) for i, interval in enumerate(interval_split)])
435
# pool.close()
436
results = sorted(results, key=lambda x: x[0])
437
for _, result in results:
438
outf.write("".join(result[-2]))
439
if mismatchf is not None:
440
mismatchf.write("".join(result[-1]))
441
return results
442
443
444
def inTaz(options, edge, tazID, isOrigin):
445
if not hasattr(options, "tazEdges"):
446
# tazID -> (originEdges, destEdges)
447
options.tazEdges = defaultdict(lambda: (set(), set()))
448
for tazFile in options.tazFiles:
449
for taz in sumolib.xml.parse(tazFile, 'taz'):
450
if taz.edges:
451
edgeIDs = taz.edges.split()
452
options.tazEdges[taz.id] = (set(edgeIDs), set(edgeIDs))
453
if taz.tazSource:
454
for ts in taz.tazSource:
455
options.tazEdges[taz.id][0].add(ts.id)
456
if taz.tazSink:
457
for ts in taz.tazSink:
458
options.tazEdges[taz.id][1].add(ts.id)
459
result = edge in options.tazEdges[tazID][0 if isOrigin else 1]
460
# print(edge, tazID, isOrigin, options.tazEdges[tazID][0 if isOrigin else 1], result)
461
return result
462
463
464
def getIntervals(options):
465
begin, end, interval = parseTimeRange(options.turnFiles + options.turnRatioFiles
466
+ options.edgeDataFiles + options.odFiles)
467
if options.begin is not None:
468
begin = parseTime(options.begin)
469
if options.end is not None:
470
end = parseTime(options.end)
471
if options.interval is not None:
472
interval = parseTime(options.interval)
473
474
# init departDist after begin and end are known, store in options for
475
# easier handover to solveInterval
476
options.departDist = None
477
if hasattr(options, "departDistVals") and options.departDistVals:
478
options.departDist = DepartDist(options.departDistVals, begin, end)
479
480
result = []
481
while begin < end:
482
result.append((begin, begin + interval))
483
begin += interval
484
485
return result
486
487
488
def getOverlap(begin, end, iBegin, iEnd):
489
"""return overlap of the given intervals as fraction"""
490
if iEnd <= begin or end <= iBegin:
491
return 0 # no overlap
492
elif iBegin >= begin and iEnd <= end:
493
return 1 # data interval fully within requested interval
494
elif iBegin <= begin and iEnd >= end:
495
return (end - begin) / (iEnd - iBegin) # only part of the data interval applies to the requested interval
496
elif iBegin <= begin and iEnd <= end:
497
return (iEnd - begin) / (iEnd - iBegin) # partial overlap
498
else:
499
return (end - iBegin) / (iEnd - iBegin) # partial overlap
500
501
502
def parseTurnCounts(interval, attr, warn):
503
if interval.edgeRelation is not None:
504
for edgeRel in interval.edgeRelation:
505
via = [] if edgeRel.via is None else edgeRel.via.split(' ')
506
edges = tuple([edgeRel.attr_from] + via + [edgeRel.to])
507
value = [getattr(edgeRel, a) for a in attr]
508
yield edges, value
509
elif interval.tazRelation is None and warn:
510
sys.stderr.write("Warning: No edgeRelations in interval from=%s to=%s\n" % (interval.begin, interval.end))
511
512
513
def parseTazCounts(interval, attr, warn):
514
if interval.tazRelation is not None:
515
for tazRel in interval.tazRelation:
516
tazs = tuple([tazRel.attr_from] + [tazRel.to])
517
value = [getattr(tazRel, a) for a in attr]
518
yield tazs, value
519
elif interval.edgeRelation is None and warn:
520
sys.stderr.write("Warning: No tazRelations in interval from=%s to=%s\n" % (interval.begin, interval.end))
521
522
523
def parseEdgeCounts(interval, attr, warn):
524
if interval.edge is not None:
525
for edge in interval.edge:
526
yield (edge.id,), [getattr(edge, a) for a in attr]
527
elif warn:
528
sys.stderr.write("Warning: No edges in interval from=%s to=%s\n" % (interval.begin, interval.end))
529
530
531
def parseDataIntervals(parseFun, fnames, begin, end, allRoutes, attr, options,
532
isOrigin=False, isDest=False, isRatio=False, isTaz=False, warn=False):
533
locations = {} # edges -> CountData
534
result = []
535
if attr is None or attr == "None":
536
return result
537
for fname in fnames:
538
for interval in sumolib.xml.parse(fname, 'interval', heterogeneous=True):
539
iBegin = parseTime(interval.begin)
540
iEnd = parseTime(interval.end)
541
overlap = 1 if isRatio else getOverlap(begin, end, iBegin, iEnd)
542
if overlap > 0:
543
# print(begin, end, interval.begin, interval.end, "overlap:", overlap)
544
for edges, value in parseFun(interval, attr, warn):
545
try:
546
value = sum([float(v) for v in value])
547
except TypeError:
548
if warn:
549
print("Warning: Missing '%s' value in file '%s' for edge(s) '%s'" %
550
(attr, fname, ' '.join(edges)), file=sys.stderr)
551
continue
552
if edges not in locations:
553
result.append(CountData(0, edges, allRoutes, isOrigin, isDest, isRatio, isTaz, options))
554
locations[edges] = result[-1]
555
elif (isOrigin and isDest) != (locations[edges].isOrigin and locations[edges].isDest):
556
print("Warning: Edge relation '%s' occurs as turn relation and also as OD-relation" %
557
' '.join(edges), file=sys.stderr)
558
elif isRatio != locations[edges].isRatio:
559
print("Warning: Edge relation '%s' occurs as turn relation and also as turn-ratio" %
560
' '.join(edges), file=sys.stderr)
561
elif not warn and overlap == 1 and begin == iBegin and end == iEnd:
562
# In 'Warn' mode we are parsing the whole time range at once so duplicate occurences
563
# are expected. Hence we warn only in the context of solveInterval where 'warn=False'.
564
print("Edge %s'%s' occurs more than once in interval %s-%s" % (
565
"relation " if len(edges) > 1 else "",
566
' '.join(edges),
567
interval.begin, interval.end),
568
file=sys.stderr)
569
value *= overlap
570
if not isRatio:
571
value = int(value)
572
if value < 0:
573
if warn:
574
print("Ignoring negative count %s for edge(s) '%s'" % (
575
value, " ".join(edges)), file=sys.stderr)
576
else:
577
locations[edges].addCount(value)
578
return result
579
580
581
def parseTimeRange(fnames):
582
begin = 1e20
583
end = 0
584
minInterval = 1e20
585
for fname in fnames:
586
for interval in sumolib.xml.parse(fname, 'interval'):
587
iBegin = parseTime(interval.begin)
588
iEnd = parseTime(interval.end)
589
begin = min(begin, iBegin)
590
end = max(end, iEnd)
591
minInterval = min(minInterval, iEnd - iBegin)
592
return begin, end, minInterval
593
594
595
def hasCapacity(dataIndices, countData):
596
for i in dataIndices:
597
if countData[i].count <= 0:
598
return False
599
return True
600
601
602
def updateOpenRoutes(openRoutes, routeUsage, countData):
603
return list(filter(lambda r: hasCapacity(routeUsage[r], countData), openRoutes))
604
605
606
def updateOpenCounts(openCounts, countData, openRoutes):
607
return list(filter(lambda i: countData[i].routeSet.intersection(openRoutes), openCounts))
608
609
610
def optimize(options, countData, routes, priorRouteCounts, routeUsage, intervalCount, rng):
611
""" use relaxtion of the ILP problem for picking the number of times that each route is used
612
x = usageCount vector (count for each route index)
613
c = weight vector (vector of 1s)
614
A_eq = routeUsage encoding
615
b_eq = counts
616
617
Rationale:
618
c: costs for using each route,
619
when minimizing x @ c, routes that pass multiple counting stations are getting an advantage
620
621
"""
622
import scipy.optimize as opt
623
624
m = len(countData)
625
626
relevantRoutes = [i for i in range(routes.number) if len(routeUsage[i]) >= options.minCount]
627
priorRelevantRouteCounts = [priorRouteCounts[r] for r in relevantRoutes]
628
k = len(relevantRoutes)
629
630
if options.optimize == "full":
631
# allow changing all prior usedRoutes
632
bounds = None
633
else:
634
u = int(options.optimize)
635
if u == 0:
636
print("Optimization skipped")
637
return
638
# limited optimization: change prior routeCounts by at most u per route
639
bounds = [(max(0, p - u), p + u) for p in priorRelevantRouteCounts] + [(0, None)] * m
640
641
# Ax <= b
642
# Ax + s = b
643
# min s
644
# -> x2 = [x, s]
645
646
A = np.zeros((m, k))
647
for i in range(0, m):
648
for j in range(0, k):
649
A[i][j] = int(relevantRoutes[j] in countData[i].routeSet)
650
A_eq = np.concatenate((A, np.identity(m)), 1)
651
652
# constraint: achieve counts
653
b = np.asarray([cd.origCount for cd in countData])
654
655
# minimization objective [routeCounts] + [slack]
656
c = [options.minimizeVehs] * k + [1] * m
657
658
A_ub = None
659
b_ub = None
660
661
if intervalCount is not None:
662
# add inequality to ensure that we stay below intervalCount
663
# sum of routes < intervalCount
664
# A_ub * x <= b_ub
665
A_ub = np.concatenate((np.ones((1, k)), np.zeros((1, m))), 1)
666
b_ub = np.asarray([intervalCount])
667
668
# print("k=%s" % k)
669
# print("m=%s" % m)
670
# print("A_eq (%s) %s" % (A_eq.shape, A_eq))
671
# print("b (%s) %s" % (len(b), b))
672
# print("c (%s) %s" % (len(c), c))
673
# print("bounds (%s) %s" % (len(bounds) if bounds is not None else "-", bounds))
674
675
linProgOpts = {}
676
if options.verbose:
677
linProgOpts["disp"] = True
678
679
res = opt.linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b, bounds=bounds, options=linProgOpts)
680
681
if res.success:
682
print("Optimization succeeded")
683
routeCountsR = res.x[:k] # cut of slack variables
684
# translate to original route indices
685
# routeCounts = [0] * routes.number
686
routeCountsRounded = [0] * routes.number
687
for index, count in zip(relevantRoutes, routeCountsR):
688
# routeCounts[index] = count
689
frac, countRound = modf(count)
690
if rng.random() < frac:
691
countRound += 1
692
routeCountsRounded[index] = int(countRound)
693
694
# print("priorRouteCounts", priorRouteCounts)
695
# print("relevantRoutes", relevantRoutes)
696
# print("priorRelevantRouteCounts", priorRelevantRouteCounts)
697
# print("routeCountsR", routeCountsR)
698
# print("routeCounts", routeCounts)
699
# slack = res.x[k:]
700
# print("routeCounts (n=%s, sum=%s, intSum=%s, roundSum=%s) %s" % (
701
# len(routeCounts),
702
# sum(routeCounts),
703
# sum(map(int, routeCounts)),
704
# sum(map(round, routeCounts)),
705
# routeCounts))
706
# print("slack (n=%s, sum=%s) %s" % (len(slack), sum(slack), slack))
707
return routeCountsRounded
708
# print("#usedRoutes=%s" % len(usedRoutes))
709
# update countData
710
else:
711
print("Optimization failed")
712
return priorRouteCounts
713
714
715
def zero():
716
# cannot pickle lambda for multiprocessing
717
return 0
718
719
720
def initTurnRatioSiblings(routes, countData, turnTotal):
721
ratioIndices = set()
722
# todo: use routes to complete incomplete sibling lists
723
ratioOrigins = defaultdict(list)
724
for cd in countData:
725
if cd.isRatio:
726
ratioOrigins[cd.edgeTuple[0]].append(cd)
727
ratioIndices.add(cd.index)
728
cd.origCount /= turnTotal
729
730
for edge, cDs in ratioOrigins.items():
731
for cd in cDs:
732
cd.ratioSiblings = cDs
733
return ratioIndices
734
735
736
def resetCounts(usedRoutes, routeUsage, countData):
737
for cd in countData:
738
cd.count = cd.origCount
739
cd.assignedCount = 0
740
for r, usage in enumerate(usedRoutes):
741
for i in routeUsage[r]:
742
countData[i].use(usage)
743
744
745
def negateCounts(countData):
746
for cd in countData:
747
cd.count *= -1
748
cd.assignedCount *= -1
749
750
751
def getRouteUsage(routes, countData):
752
"""store which counting locations are used by each route (using countData index)"""
753
routeUsage = [set() for r in routes.unique]
754
for i, cd in enumerate(countData):
755
for routeIndex in cd.routeSet:
756
routeUsage[routeIndex].add(i)
757
return routeUsage
758
759
760
def initTotalCounts(options, routes, intervals, b, e):
761
"""initialize time line for options.totalCount if the number of input vehicles/persons should be preserved
762
or in case a single value was given for multiple intervals
763
"""
764
if options.totalCount == PRESERVE_INPUT_COUNT:
765
element = 'person' if options.pedestrians else 'vehicle'
766
options.totalCount = [0] * len(intervals)
767
interval = e - b if len(intervals) == 1 else intervals[0][1] - intervals[0][0]
768
numVehs = 0
769
numExcluded = 0
770
for routefile in options.routeFiles:
771
for veh in sumolib.xml.parse(routefile, [element], heterogeneous=True):
772
numVehs += 1
773
depart = parseTime(veh.depart)
774
if depart >= b and depart <= e:
775
iIndex = int((depart - b) / interval)
776
if depart == e:
777
iIndex -= 1
778
assert iIndex < len(options.totalCount)
779
options.totalCount[iIndex] += 1
780
else:
781
numExcluded += 1
782
if options.verbose:
783
if len(intervals) == 1:
784
print("Using total count of %s corresponding to input %s count" % (
785
numVehs - numExcluded, element))
786
else:
787
print("Using total count of %s in proportion to input %s counts: %s" % (
788
numVehs - numExcluded, element, ','.join(map(str, options.totalCount))))
789
if numExcluded > 0:
790
print("Ignored %s %ss because they depart outside the configured time range [%s, %s]" %
791
(numExcluded, element, humanReadableTime(b), humanReadableTime(e)),
792
file=sys.stderr)
793
794
elif len(options.totalCount) != len(intervals):
795
if len(options.totalCount) == 1:
796
# split proportionally
797
countSums = []
798
for begin, end in intervals:
799
countData = parseCounts(options, routes, begin, end)
800
countSums.append(sum(cd.origCount for cd in countData))
801
countSumTotal = sum(countSums)
802
origTotal = options.totalCount[0]
803
options.totalCount = [int(ceil(origTotal * s / countSumTotal)) for s in countSums]
804
if options.verbose:
805
print("Splitting total count of %s in proportion to interval counting data: %s" % (
806
origTotal, ','.join(map(str, options.totalCount))))
807
else:
808
sys.stderr.write("Error: --total-count must be a single value" +
809
" or match the number of data intervals (%s)" % len(intervals))
810
sys.exit()
811
812
813
def _sample_skewed(sampleSet, rng, probabilityMap):
814
# build cumulative distribution function for weighted sampling
815
cdf = []
816
total = 0
817
population = tuple(sampleSet)
818
for element in population:
819
total += probabilityMap[element]
820
cdf.append(total)
821
822
value = rng.rand() * total
823
return population[np.searchsorted(cdf, value)]
824
825
826
def _solveIntervalMP(options, routes, interval, cpuIndex):
827
output_list = []
828
rng = np.random.RandomState(options.seed + cpuIndex)
829
for i, (begin, end) in enumerate(interval):
830
local_outf = StringIO()
831
local_mismatch_outf = StringIO() if options.mismatchOut else None
832
intervalPrefix = "%s_" % int(begin)
833
intervalCount = options.totalCount[i] if options.totalCount else None
834
uFlow, oFlow, gehOKPerc, ratioPerc, inputCount, usedRoutes, local_outf = solveInterval(
835
options, routes, begin, end, intervalPrefix, local_outf, local_mismatch_outf, rng, intervalCount)
836
837
output_list.append([begin, uFlow, oFlow, gehOKPerc, ratioPerc, inputCount, usedRoutes, local_outf.getvalue(),
838
local_mismatch_outf.getvalue() if options.mismatchOut else None])
839
output_lst = list(zip(*output_list))
840
return output_lst
841
842
843
def parseCounts(options, routes, b, e, warn=False):
844
countData = (parseDataIntervals(parseTurnCounts, options.turnFiles, b, e,
845
routes, options.turnAttr, options=options, warn=warn)
846
+ parseDataIntervals(parseTurnCounts, options.turnRatioFiles, b, e,
847
routes, options.turnRatioAttr, options=options, isRatio=True, warn=warn)
848
+ parseDataIntervals(parseEdgeCounts, options.edgeDataFiles, b, e,
849
routes, options.edgeDataAttr, options=options, warn=warn)
850
+ parseDataIntervals(parseTurnCounts, options.odFiles, b, e,
851
routes, options.turnAttr, options=options,
852
isOrigin=True, isDest=True, warn=warn)
853
+ parseDataIntervals(parseEdgeCounts, options.edgeDataFiles, b, e,
854
routes, options.departAttr, options=options, isOrigin=True, warn=warn)
855
+ parseDataIntervals(parseEdgeCounts, options.edgeDataFiles, b, e,
856
routes, options.arrivalAttr, options=options, isDest=True, warn=warn)
857
)
858
if options.tazFiles is not None:
859
countData += parseDataIntervals(parseTazCounts, options.odFiles, b, e,
860
routes, options.turnAttr, options=options,
861
isTaz=True, warn=warn)
862
for i, cd in enumerate(countData):
863
cd.index = i
864
return countData
865
866
867
def hasODCount(cdIndices, countData):
868
for i in cdIndices:
869
cd = countData[i]
870
if cd.isTaz or (cd.isOrigin and cd.isDest):
871
return True
872
return False
873
874
875
def initOpen(options, routes, routeUsage, countData, unrestricted):
876
openRoutes = updateOpenRoutes(range(0, routes.number), routeUsage, countData)
877
openRoutes = [r for r in openRoutes if r not in unrestricted]
878
if options.odFiles and not options.extraOD:
879
openRoutes = [r for r in openRoutes if hasODCount(routeUsage[r], countData)]
880
openCounts = updateOpenCounts(range(0, len(countData)), countData, openRoutes)
881
return openRoutes, openCounts
882
883
884
def getHourFraction(options, begin, end):
885
if options.gehScale is None:
886
return (end - begin) / 3600.0
887
else:
888
return 1 / options.gehScale
889
890
891
def solveInterval(options, routes, begin, end, intervalPrefix, outf, mismatchf, rng, intervalCount):
892
countData = parseCounts(options, routes, begin, end)
893
894
ratioIndices = None
895
if options.turnRatioFiles:
896
ratioIndices = initTurnRatioSiblings(routes, countData, options.turnRatioTotal)
897
898
routeUsage = getRouteUsage(routes, countData)
899
unrestricted_list = [r for r, usage in enumerate(routeUsage) if len(usage) < options.minCount]
900
unrestricted = set(unrestricted_list)
901
if options.verbose and len(unrestricted) > 0:
902
if options.minCount == 1:
903
print("Ignored %s routes which do not pass any counting location" % len(unrestricted))
904
else:
905
print("Ignored %s routes which pass fewer than %s counting location" % (
906
len(unrestricted), options.minCount))
907
908
openRoutes, openCounts = initOpen(options, routes, routeUsage, countData, unrestricted)
909
910
routeCounts = [0] * routes.number # hold the use-count for each route
911
numSampled = 0
912
if options.initInput:
913
for e in routes.all:
914
routeCounts[routes.edges2index[e]] += 1
915
resetCounts(routeCounts, routeUsage, countData)
916
917
if options.initInputRemove:
918
fully_unrestricted = set([r for r, usage in enumerate(routeUsage) if len(usage) == 0])
919
negateCounts(countData)
920
openRoutes, openCounts = initOpen(options, routes, routeUsage, countData, fully_unrestricted)
921
routeCounts, numSampled = sampleRoutes(options, rng, routes, countData, routeUsage, openRoutes, openCounts,
922
None, numSampled, intervalCount, routeCounts, remove=True)
923
if numSampled < 0:
924
print(" Removed %s routes from input to reduce overflow" % -numSampled)
925
negateCounts(countData)
926
927
openRoutes, openCounts = initOpen(options, routes, routeUsage, countData, unrestricted)
928
929
if options.optimize != "full" and not options.noSampling:
930
routeCounts, numSampled = sampleRoutes(options, rng, routes, countData, routeUsage, openRoutes, openCounts,
931
ratioIndices, numSampled, intervalCount, routeCounts)
932
933
totalMismatch = sum([cd.count for cd in countData]) # noqa
934
935
if totalMismatch > 0 and options.optimize is not None:
936
if options.verbose:
937
print("Starting optimization for interval [%s, %s] (mismatch %s)" % (
938
begin, end, totalMismatch))
939
routeCounts = optimize(options, countData, routes, routeCounts, routeUsage, intervalCount, rng)
940
resetCounts(routeCounts, routeUsage, countData)
941
numSampled = sum(routeCounts)
942
943
if intervalCount is not None and numSampled < intervalCount:
944
if unrestricted:
945
if options.minCount != 1:
946
# ensure that we only sample from routes that do not pass any counting locations
947
unrestricted_list = [r for r, usage in enumerate(routeUsage) if len(usage) == 0]
948
unrestricted = set(unrestricted_list)
949
950
while numSampled < intervalCount:
951
if options.weighted:
952
routeIndex = _sample_skewed(unrestricted, rng, routes.probabilities)
953
else:
954
routeIndex = rng.choice(unrestricted_list)
955
routeCounts[routeIndex] += 1
956
assert len(routeUsage[routeIndex]) == 0
957
numSampled += 1
958
else:
959
print("Cannot fulfill total interval count of %s due to lack of unrestricted routes" % intervalCount,
960
file=sys.stderr)
961
962
if any(routeCounts):
963
usedRoutes = writeRoutes(options, rng, outf, routes, routeCounts, begin, end, intervalPrefix)
964
else:
965
usedRoutes = []
966
967
underflow = sumolib.miscutils.Statistics("underflow locations")
968
overflow = sumolib.miscutils.Statistics("overflow locations")
969
gehStats = sumolib.miscutils.Statistics("GEH")
970
ratioStats = sumolib.miscutils.Statistics("turnRatio")
971
numGehOK = 0.0
972
gehOKPerc = 100
973
ratioPerc = None
974
hourFraction = getHourFraction(options, begin, end)
975
totalCount = 0
976
totalOrigCount = 0
977
totalRatioCount = 0
978
for cd in countData:
979
if cd.isRatio:
980
aProb = setPrecision("%.2f", options.precision) % cd.assignedProbability()
981
cdID = "[%s] %s %s" % (' '.join(cd.edgeTuple), cd.origCount, aProb)
982
ratioStats.add(cd.assignedProbability() - cd.origCount, cdID)
983
totalRatioCount += cd.assignedCount
984
continue
985
localCount = cd.origCount - cd.count
986
totalCount += localCount
987
totalOrigCount += cd.origCount
988
if cd.count > 0:
989
underflow.add(cd.count, cd.edgeTuple)
990
elif cd.count < 0:
991
overflow.add(cd.count, cd.edgeTuple)
992
origHourly = cd.origCount / hourFraction
993
localHourly = localCount / hourFraction
994
geh = sumolib.miscutils.geh(origHourly, localHourly)
995
if geh < options.gehOk:
996
numGehOK += 1
997
cdID = "[%s] %s %s" % (' '.join(cd.edgeTuple), int(origHourly), int(localHourly))
998
gehStats.add(geh, cdID)
999
1000
gehInfo = ""
1001
ratioInfo = ""
1002
if gehStats.count() > 0:
1003
countPercentage = "%.2f%%" % (100 * totalCount / float(totalOrigCount)) if totalOrigCount else "-"
1004
gehOKPerc = 100 * numGehOK / float(gehStats.count()) if countData else 100
1005
gehOK = "%.2f%%" % gehOKPerc if countData else "-"
1006
gehInfo = "total count %s (%s) at %s locations. GEH<%s for %s" % (
1007
totalCount, countPercentage,
1008
gehStats.count(),
1009
options.gehOk, gehOK)
1010
1011
if ratioStats.count() > 0:
1012
if gehStats.count() > 0:
1013
ratioInfo = " and "
1014
ratioPerc = ratioStats.avg_abs() * 100
1015
ratioInfo += setPrecision("avg ratio mismatch %.2f%% at %s ratio locations (count %s)", options.precision) % (
1016
ratioPerc, ratioStats.count(), totalRatioCount)
1017
1018
outputIntervalPrefix = "" if intervalPrefix == "" else "%s: " % int(begin)
1019
print("%sWrote %s routes (%s distinct) %s%s%s" % (
1020
outputIntervalPrefix,
1021
sum(routeCounts), len([c for c in routeCounts if c != 0]),
1022
("achieving " if countData else "no data"),
1023
gehInfo, ratioInfo))
1024
1025
if options.verboseHistogram:
1026
edgeCount = sumolib.miscutils.Statistics("route edge count", histogram=True)
1027
detectorCount = sumolib.miscutils.Statistics("route detector count", histogram=True)
1028
for i, r in enumerate(usedRoutes):
1029
edgeCount.add(len(routes.unique[r]), i)
1030
detectorCount.add(len(routeUsage[r]), i)
1031
print("result %s" % edgeCount)
1032
print("result %s" % detectorCount)
1033
print(gehStats)
1034
1035
if underflow.count() > 0:
1036
print("Warning: %s (total %s)" % (underflow, sum(underflow.values)))
1037
if overflow.count() > 0:
1038
print("Warning: %s (total %s)" % (overflow, sum(overflow.values)))
1039
sys.stdout.flush() # needed for multiprocessing
1040
1041
if mismatchf:
1042
writeMismatch(options, mismatchf, countData, begin, end)
1043
1044
return sum(underflow.values), sum(overflow.values), gehOKPerc, ratioPerc, totalOrigCount, routeCounts, outf
1045
1046
1047
def sampleRoutes(options, rng, routes, countData, routeUsage, openRoutes, openCounts,
1048
ratioIndices, numSampled, intervalCount, routeCounts, remove=False):
1049
"""pick a random counting location and select a new route that passes it until
1050
all counts are satisfied or no routes can be used anymore """
1051
while openCounts:
1052
if intervalCount is not None and numSampled >= intervalCount:
1053
break
1054
1055
if ratioIndices and intervalCount is None:
1056
realCounts = [cdi for cdi in openCounts if cdi not in ratioIndices]
1057
if not realCounts:
1058
if numSampled == 0:
1059
print("Stopped sampling routes because only ratios are still open."
1060
+ " Set option --total-count to sample ratios without local counts",
1061
file=sys.stderr)
1062
break
1063
1064
if options.weighted:
1065
routeIndex = _sample_skewed(openRoutes, rng, routes.probabilities)
1066
else:
1067
# sampling equally among open counting locations appears to
1068
# improve GEH but it would also introduce a bias in the loaded
1069
# route probabilities
1070
cd = countData[rng.choice(openCounts)]
1071
routeIndex = rng.choice([r for r in openRoutes if r in cd.routeSet])
1072
if remove:
1073
numSampled -= 1
1074
routeCounts[routeIndex] -= 1
1075
else:
1076
numSampled += 1
1077
routeCounts[routeIndex] += 1
1078
1079
for dataIndex in routeUsage[routeIndex]:
1080
countData[dataIndex].use(1)
1081
1082
if ratioIndices:
1083
for dataIndex in routeUsage[routeIndex]:
1084
countData[dataIndex].updateTurnRatioCounts(openRoutes, openCounts, True)
1085
1086
# this is the old and slow way to update things
1087
openRoutes = updateOpenRoutes(openRoutes, routeUsage, countData)
1088
openCounts = updateOpenCounts(openCounts, countData, openRoutes)
1089
1090
else:
1091
# update openRouts and openCounts only if needed
1092
closedRoutes = set()
1093
for dataIndex in routeUsage[routeIndex]:
1094
cd = countData[dataIndex]
1095
if cd.count == 0:
1096
openCounts.remove(dataIndex)
1097
for r in cd.routeSet:
1098
closedRoutes.add(r)
1099
1100
if remove and routeCounts[routeIndex] == 0:
1101
closedRoutes.add(routeIndex)
1102
1103
if closedRoutes:
1104
cdRecheck = set()
1105
openRoutes2 = []
1106
for r in openRoutes:
1107
if r in closedRoutes:
1108
for dataIndex in routeUsage[r]:
1109
cdRecheck.add(dataIndex)
1110
else:
1111
openRoutes2.append(r)
1112
openRoutes = openRoutes2
1113
closedCounts = [c for c in cdRecheck if not countData[c].routeSet.intersection(openRoutes)]
1114
if closedCounts:
1115
openCounts = [c for c in openCounts if c not in closedCounts]
1116
1117
return routeCounts, numSampled
1118
1119
1120
def writeRoutes(options, rng, outf, routes, routeCounts, begin, end, intervalPrefix):
1121
outf.write('<!-- begin="%s" end="%s" -->\n' % (begin, end))
1122
usedRoutes = [] # simple list of route indices with
1123
for r, usage in enumerate(routeCounts):
1124
usedRoutes += [r] * usage
1125
rng.shuffle(usedRoutes)
1126
1127
if options.writeRouteIDs:
1128
for routeIndex in sorted(set(usedRoutes)):
1129
routes.write(outf, options.prefix, intervalPrefix, routeIndex, routeCounts[routeIndex])
1130
outf.write('\n')
1131
elif options.writeRouteDist:
1132
outf.write(' <routeDistribution id="%s%s%s">\n' % (
1133
options.prefix, intervalPrefix, options.writeRouteDist))
1134
for routeIndex in sorted(set(usedRoutes)):
1135
routes.write(outf, options.prefix, intervalPrefix, routeIndex,
1136
routeCounts[routeIndex], writeDist=True)
1137
outf.write(' </routeDistribution>\n\n')
1138
1139
routeID = options.writeRouteDist
1140
1141
if options.writeFlows is None:
1142
if options.departDist:
1143
departs = options.departDist.sample(rng, len(usedRoutes), begin, end)
1144
else:
1145
departs = [rng.uniform(begin, end) for ri in usedRoutes]
1146
departs.sort()
1147
for i, routeIndex in enumerate(usedRoutes):
1148
if options.writeRouteIDs:
1149
routeID = routeIndex
1150
vehID = options.prefix + intervalPrefix + str(i)
1151
depart = departs[i]
1152
if routeID is not None:
1153
if options.pedestrians:
1154
outf.write(' <person id="%s" depart="%.2f"%s>\n' % (
1155
vehID, depart, options.vehattrs))
1156
outf.write(' <walk route="%s%s%s"/>\n' % (options.prefix, intervalPrefix, routeID))
1157
outf.write(' </person>\n')
1158
else:
1159
outf.write(' <vehicle id="%s" depart="%.2f" route="%s%s%s"%s/>\n' % (
1160
vehID, depart, options.prefix, intervalPrefix, routeID, options.vehattrs))
1161
else:
1162
if options.pedestrians:
1163
outf.write(' <person id="%s" depart="%.2f"%s>\n' % (
1164
vehID, depart, options.vehattrs))
1165
outf.write(' <walk edges="%s"/>\n' % ' '.join(routes.unique[routeIndex]))
1166
outf.write(' </person>\n')
1167
else:
1168
outf.write(' <vehicle id="%s" depart="%.2f"%s>\n' % (
1169
vehID, depart, options.vehattrs))
1170
routes.write(outf, None, None, routeIndex, None)
1171
outf.write(' </vehicle>\n')
1172
else:
1173
if options.writeRouteDist:
1174
totalCount = sum(routeCounts)
1175
probability = totalCount / (end - begin)
1176
fBegin = begin
1177
if options.writeFlows == "number":
1178
# don't always start at the interval begin
1179
fBegin += rng.uniform(0, 1 / probability)
1180
flowID = options.prefix + intervalPrefix + options.writeRouteDist
1181
if options.writeFlows == "poisson":
1182
repeat = 'period="exp(%.4f)"' % probability
1183
elif options.writeFlows == "number" or probability > 1.00004:
1184
repeat = 'number="%s"' % totalCount
1185
if options.writeFlows == "probability":
1186
sys.stderr.write("Warning: could not write flow %s with probability %.5f\n" %
1187
(flowID, probability))
1188
else:
1189
repeat = 'probability="%.4f"' % probability
1190
outf.write(' <flow id="%s" begin="%.2f" end="%.2f" %s route="%s"%s/>\n' % (
1191
flowID, fBegin, end, repeat,
1192
options.writeRouteDist, options.vehattrs))
1193
else:
1194
# ensure flows are sorted
1195
flows = []
1196
for routeIndex in sorted(set(usedRoutes)):
1197
outf2 = StringIO()
1198
probability = routeCounts[routeIndex] / (end - begin)
1199
fBegin = begin
1200
fEnd = end
1201
if options.writeFlows == "number":
1202
# don't always start at the interval begin
1203
fBegin += rng.uniform(0, 1 / probability)
1204
flowID = "%s%s%s" % (options.prefix, intervalPrefix, routeIndex)
1205
if options.writeFlows == "poisson":
1206
repeat = 'period="exp(%.4f)"' % probability
1207
elif options.writeFlows == "number" or probability > 1.00004:
1208
repeat = 'number="%s"' % routeCounts[routeIndex]
1209
if options.writeFlows == "probability":
1210
sys.stderr.write("Warning: could not write flow %s with probability %.5f\n" % (
1211
flowID, probability))
1212
else:
1213
repeat = 'probability="%.4f"' % probability
1214
if options.writeRouteIDs:
1215
if options.pedestrians:
1216
outf2.write(' <personFlow id="%s" begin="%.2f" end="%.2f" %s%s>\n' % (
1217
flowID, fBegin, fEnd, repeat, options.vehattrs))
1218
outf2.write(' <walk route="%s%s%s"/>\n' % (
1219
options.prefix, intervalPrefix, routeIndex))
1220
outf2.write(' </personFlow>\n')
1221
else:
1222
outf2.write(' <flow id="%s" begin="%.2f" end="%.2f" %s route="%s%s%s"%s/>\n' % (
1223
flowID, fBegin, fEnd, repeat,
1224
options.prefix, intervalPrefix, routeIndex, options.vehattrs))
1225
else:
1226
if options.pedestrians:
1227
outf2.write(' <personFlow id="%s" begin="%.2f" end="%.2f" %s%s>\n' % (
1228
flowID, fBegin, fEnd, repeat, options.vehattrs))
1229
outf2.write(' <walk edges="%s"/>\n' % ' '.join(routes.unique[routeIndex]))
1230
outf2.write(' </personFlow>\n')
1231
else:
1232
outf2.write(' <flow id="%s" begin="%.2f" end="%.2f" %s%s>\n' % (
1233
flowID, fBegin, fEnd, repeat, options.vehattrs))
1234
routes.write(outf2, None, None, routeIndex, None)
1235
outf2.write(' </flow>\n')
1236
# secondary sort by routeIndex so we don't have to compare stringIO
1237
flows.append((fBegin, routeIndex, outf2))
1238
flows.sort()
1239
for fBegin, index, outf2 in flows:
1240
outf.write(outf2.getvalue())
1241
return usedRoutes
1242
1243
1244
def writeMismatch(options, mismatchf, countData, begin, end):
1245
mismatchf.write(' <interval id="deficit" begin="%s" end="%s">\n' % (begin, end))
1246
hourFraction = getHourFraction(options, begin, end)
1247
for cd in countData:
1248
geh = None if cd.isRatio else sumolib.miscutils.geh(
1249
cd.origCount / hourFraction, (cd.origCount - cd.count) / hourFraction)
1250
if len(cd.edgeTuple) == 1:
1251
mismatchf.write(' <edge id="%s" measuredCount="%s" deficit="%s" GEH="%.2f"/>\n' % (
1252
cd.edgeTuple[0], cd.origCount, cd.count, geh))
1253
elif len(cd.edgeTuple) == 2:
1254
tag = 'tazRelation' if cd.isTaz else 'edgeRelation'
1255
relationPrefix = ' <%s from="%s" to="%s" ' % ((tag,) + cd.edgeTuple)
1256
if cd.isRatio:
1257
deficit = setPrecision("%.2f", options.precision) % (cd.assignedProbability() - cd.origCount)
1258
mismatchf.write('%smeasuredProbability="%s" deficit="%s" totalAssignedFromCount="%s"/>\n'
1259
% (relationPrefix, cd.origCount, deficit, cd.getSiblingCount()))
1260
else:
1261
mismatchf.write('%smeasuredCount="%s" deficit="%s" GEH="%.2f"/>\n'
1262
% (relationPrefix, cd.origCount, cd.count, geh))
1263
else:
1264
print("Warning: output for edge relations with more than 2 edges not supported (%s)" % cd.edgeTuple,
1265
file=sys.stderr)
1266
mismatchf.write(' </interval>\n')
1267
1268
1269
def main(options):
1270
rng = np.random.RandomState(options.seed)
1271
1272
with Benchmarker(options.verboseTiming, "Loading routes"):
1273
routes = Routes(options.routeFiles, options.keepStops, rng)
1274
1275
intervals = getIntervals(options)
1276
if len(intervals) == 0:
1277
print("Error: no intervals loaded", file=sys.stderr)
1278
sys.exit()
1279
1280
# preliminary integrity check for the whole time range
1281
b = intervals[0][0]
1282
e = intervals[-1][-1]
1283
with Benchmarker(options.verboseTiming, "Loading counts"):
1284
countData = parseCounts(options, routes, b, e, True)
1285
routeUsage = getRouteUsage(routes, countData)
1286
1287
for cd in countData:
1288
if cd.count > 0 and not cd.routeSet:
1289
msg = ""
1290
if cd.isOrigin and cd.isDest:
1291
msg = "start at edge '%s' and end at edge '%s'" % (cd.edgeTuple[0], cd.edgeTuple[-1])
1292
elif cd.isOrigin:
1293
msg = "start at edge '%s'" % cd.edgeTuple[0]
1294
elif cd.isDest:
1295
msg = "end at edge '%s'" % cd.edgeTuple[-1]
1296
elif len(cd.edgeTuple) > 1:
1297
msg = "pass edges '%s'" % ' '.join(cd.edgeTuple)
1298
else:
1299
msg = "pass edge '%s'" % ' '.join(cd.edgeTuple)
1300
print("Warning: no routes %s (count %s)" % (msg, cd.count), file=sys.stderr)
1301
1302
if options.verbose:
1303
print("Loaded %s routes (%s distinct)" % (len(routes.all), routes.number))
1304
if options.weighted:
1305
print("Loaded probability for %s routes" % routes.withProb)
1306
if options.verboseHistogram:
1307
edgeCount = sumolib.miscutils.Statistics("route edge count", histogram=True)
1308
detectorCount = sumolib.miscutils.Statistics("route detector count", histogram=True)
1309
for i, edges in enumerate(routes.unique):
1310
edgeCount.add(len(edges), i)
1311
detectorCount.add(len(routeUsage[i]), i)
1312
print("input %s" % edgeCount)
1313
print("input %s" % detectorCount)
1314
1315
mismatchf = None
1316
if options.mismatchOut:
1317
mismatchf = open(options.mismatchOut, 'w')
1318
sumolib.writeXMLHeader(mismatchf, "$Id$", options=options) # noqa
1319
mismatchf.write('<data>\n')
1320
1321
if options.totalCount:
1322
initTotalCounts(options, routes, intervals, b, e)
1323
1324
underflowSummary = sumolib.miscutils.Statistics("avg interval underflow")
1325
overflowSummary = sumolib.miscutils.Statistics("avg interval overflow")
1326
gehSummary = sumolib.miscutils.Statistics("avg interval GEH%")
1327
ratioSummary = sumolib.miscutils.Statistics("avg interval ratio mismatch%")
1328
inputCountSummary = sumolib.miscutils.Statistics("avg interval input count")
1329
usedRoutesSummary = sumolib.miscutils.Statistics("avg interval written vehs")
1330
1331
with open(options.out, 'w') as outf, Benchmarker(options.verboseTiming, "Sampling all intervals"):
1332
sumolib.writeXMLHeader(outf, "$Id$", "routes", options=options) # noqa
1333
if options.threads > 1:
1334
# call the multiprocessing function
1335
results = multi_process(options.threads, intervals,
1336
_solveIntervalMP, outf, mismatchf, options=options, routes=routes)
1337
# handle the uFlow, oFlow and GEH
1338
for _, result in results:
1339
for i, begin in enumerate(result[0]):
1340
underflowSummary.add(result[1][i], begin)
1341
overflowSummary.add(result[2][i], begin)
1342
gehSummary.add(result[3][i], begin)
1343
if result[4][i] is not None:
1344
ratioSummary.add(result[4][i], begin)
1345
inputCountSummary.add(result[5][i], begin)
1346
usedRoutesSummary.add(sum(result[6][i]), begin)
1347
else:
1348
for i, (begin, end) in enumerate(intervals):
1349
intervalPrefix = "" if len(intervals) == 1 else "%s_" % int(begin)
1350
intervalCount = options.totalCount[i] if options.totalCount else None
1351
uFlow, oFlow, gehOK, ratioPerc, inputCount, usedRoutes, _ = solveInterval(
1352
options, routes, begin, end, intervalPrefix, outf, mismatchf, rng, intervalCount)
1353
underflowSummary.add(uFlow, begin)
1354
overflowSummary.add(oFlow, begin)
1355
gehSummary.add(gehOK, begin)
1356
if ratioPerc is not None:
1357
ratioSummary.add(ratioPerc, begin)
1358
inputCountSummary.add(inputCount, begin)
1359
usedRoutesSummary.add(sum(usedRoutes), begin)
1360
outf.write('</routes>\n')
1361
1362
if options.mismatchOut:
1363
mismatchf.write('</data>\n')
1364
mismatchf.close()
1365
1366
if len(intervals) > 1:
1367
print(inputCountSummary)
1368
print(usedRoutesSummary)
1369
print(underflowSummary)
1370
print(overflowSummary)
1371
print(gehSummary)
1372
if ratioSummary.count() > 0:
1373
print(ratioSummary)
1374
1375
1376
if __name__ == "__main__":
1377
main(get_options())
1378
1379