1#!/usr/local/bin/python3.8 2# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.org/sumo 3# Copyright (C) 2010-2019 German Aerospace Center (DLR) and others. 4# This program and the accompanying materials 5# are made available under the terms of the Eclipse Public License v2.0 6# which accompanies this distribution, and is available at 7# http://www.eclipse.org/legal/epl-v20.html 8# SPDX-License-Identifier: EPL-2.0 9 10# @file randomTrips.py 11# @author Daniel Krajzewicz 12# @author Jakob Erdmann 13# @author Michael Behrisch 14# @date 2010-03-06 15# @version $Id$ 16 17 18from __future__ import print_function 19from __future__ import absolute_import 20import os 21import sys 22import random 23import bisect 24import subprocess 25from collections import defaultdict 26import math 27import optparse 28 29if 'SUMO_HOME' in os.environ: 30 sys.path.append(os.path.join(os.environ['SUMO_HOME'], 'tools')) 31import sumolib # noqa 32from sumolib.miscutils import euclidean # noqa 33 34DUAROUTER = sumolib.checkBinary('duarouter') 35 36SOURCE_SUFFIX = ".src.xml" 37SINK_SUFFIX = ".dst.xml" 38VIA_SUFFIX = ".via.xml" 39 40 41def get_options(args=None): 42 optParser = optparse.OptionParser() 43 optParser.add_option("-n", "--net-file", dest="netfile", 44 help="define the net file (mandatory)") 45 optParser.add_option("-a", "--additional-files", dest="additional", 46 help="define additional files to be loaded by the router") 47 optParser.add_option("-o", "--output-trip-file", dest="tripfile", 48 default="trips.trips.xml", help="define the output trip filename") 49 optParser.add_option("-r", "--route-file", dest="routefile", 50 help="generates route file with duarouter") 51 optParser.add_option("--weights-prefix", dest="weightsprefix", 52 help="loads probabilities for being source, destination and via-edge from the files named " + 53 "<prefix>.src.xml, <prefix>.sink.xml and <prefix>.via.xml") 54 optParser.add_option("--weights-output-prefix", dest="weights_outprefix", 55 help="generates weights files for visualisation") 56 optParser.add_option("--pedestrians", action="store_true", 57 default=False, help="create a person file with pedestrian trips instead of vehicle trips") 58 optParser.add_option("--persontrips", action="store_true", 59 default=False, help="create a person file with person trips instead of vehicle trips") 60 optParser.add_option("--persontrip.transfer.car-walk", dest="carWalkMode", 61 help="Where are mode changes from car to walking allowed " + 62 "(possible values: 'ptStops', 'allJunctions' and combinations)") 63 optParser.add_option("--persontrip.walkfactor", dest="walkfactor", 64 help="Use FLOAT as a factor on pedestrian maximum speed during intermodal routing") 65 optParser.add_option("--prefix", dest="tripprefix", 66 default="", help="prefix for the trip ids") 67 optParser.add_option("-t", "--trip-attributes", dest="tripattrs", 68 default="", help="additional trip attributes. When generating pedestrians, attributes for " + 69 "<person> and <walk> are supported.") 70 optParser.add_option("--fringe-start-attributes", dest="fringeattrs", 71 default="", help="additional trip attributes when starting on a fringe.") 72 optParser.add_option("-b", "--begin", type="float", default=0, help="begin time") 73 optParser.add_option("-e", "--end", type="float", default=3600, help="end time (default 3600)") 74 optParser.add_option( 75 "-p", "--period", type="float", default=1, help="Generate vehicles with equidistant departure times and " + 76 "period=FLOAT (default 1.0). If option --binomial is used, the expected arrival rate is set to 1/period.") 77 optParser.add_option("-s", "--seed", type="int", help="random seed") 78 optParser.add_option("-l", "--length", action="store_true", 79 default=False, help="weight edge probability by length") 80 optParser.add_option("-L", "--lanes", action="store_true", 81 default=False, help="weight edge probability by number of lanes") 82 optParser.add_option("--edge-param", dest="edgeParam", 83 help="use the given edge parameter as factor for edge") 84 optParser.add_option("--speed-exponent", type="float", dest="speed_exponent", 85 default=0.0, help="weight edge probability by speed^<FLOAT> (default 0)") 86 optParser.add_option("--fringe-factor", type="float", dest="fringe_factor", 87 default=1.0, help="multiply weight of fringe edges by <FLOAT> (default 1") 88 optParser.add_option("--fringe-threshold", type="float", dest="fringe_threshold", 89 default=0.0, help="only consider edges with speed above <FLOAT> as fringe edges (default 0)") 90 optParser.add_option("--allow-fringe", dest="allow_fringe", action="store_true", 91 default=False, help="Allow departing on edges that leave the network and arriving on edges " + 92 "that enter the network (via turnarounds or as 1-edge trips") 93 optParser.add_option("--allow-fringe.min-length", type="float", dest="allow_fringe_min_length", 94 help="Allow departing on edges that leave the network and arriving on edges " + 95 "that enter the network, if they have at least the given length") 96 optParser.add_option("--min-distance", type="float", dest="min_distance", 97 default=0.0, help="require start and end edges for each trip to be at least <FLOAT> m apart") 98 optParser.add_option("--max-distance", type="float", dest="max_distance", 99 default=None, help="require start and end edges for each trip to be at most <FLOAT> m " + 100 "apart (default 0 which disables any checks)") 101 optParser.add_option("-i", "--intermediate", type="int", 102 default=0, help="generates the given number of intermediate way points") 103 optParser.add_option("--flows", type="int", 104 default=0, help="generates INT flows that together output vehicles with the specified period") 105 optParser.add_option("--jtrrouter", action="store_true", 106 default=False, help="Create flows without destination as input for jtrrouter") 107 optParser.add_option("--maxtries", type="int", 108 default=100, help="number of attemps for finding a trip which meets the distance constraints") 109 optParser.add_option("--binomial", type="int", metavar="N", 110 help="If this is set, the number of departures per seconds will be drawn from a binomial " + 111 "distribution with n=N and p=PERIOD/N where PERIOD is the argument given to " + 112 "option --period. Tnumber of attemps for finding a trip which meets the distance constraints") 113 optParser.add_option( 114 "-c", "--vclass", "--edge-permission", default="passenger", 115 help="only from and to edges which permit the given vehicle class") 116 optParser.add_option( 117 "--vehicle-class", help="The vehicle class assigned to the generated trips (adds a standard vType definition " + 118 "to the output file).") 119 optParser.add_option("--validate", default=False, action="store_true", 120 help="Whether to produce trip output that is already checked for connectivity") 121 optParser.add_option("-v", "--verbose", action="store_true", 122 default=False, help="tell me what you are doing") 123 (options, args) = optParser.parse_args(args=args) 124 if not options.netfile: 125 optParser.print_help() 126 sys.exit(1) 127 128 if options.persontrips: 129 options.pedestrians = True 130 131 if options.pedestrians: 132 options.vclass = 'pedestrian' 133 if options.flows > 0: 134 print("Error: Person flows are not supported yet", file=sys.stderr) 135 sys.exit(1) 136 137 if options.validate and options.routefile is None: 138 options.routefile = "routes.rou.xml" 139 140 if options.period <= 0: 141 print("Error: Period must be positive", file=sys.stderr) 142 sys.exit(1) 143 144 if options.jtrrouter and options.flows <= 0: 145 print("Error: Option --jtrrouter must be used with option --flows", file=sys.stderr) 146 sys.exit(1) 147 148 if options.vehicle_class: 149 if options.tripprefix: 150 options.vtypeID = "%s_%s" % (options.tripprefix, options.vehicle_class) 151 else: 152 options.vtypeID = options.vehicle_class 153 154 if 'type=' in options.tripattrs: 155 print("Error: trip-attribute 'type' cannot be used together with option --vehicle-class", file=sys.stderr) 156 sys.exit(1) 157 158 return options 159 160 161class InvalidGenerator(Exception): 162 pass 163 164# assigns a weight to each edge using weight_fun and then draws from a discrete 165# distribution with these weights 166 167 168class RandomEdgeGenerator: 169 170 def __init__(self, net, weight_fun): 171 self.net = net 172 self.weight_fun = weight_fun 173 self.cumulative_weights = [] 174 self.total_weight = 0 175 for edge in self.net._edges: 176 # print edge.getID(), weight_fun(edge) 177 self.total_weight += weight_fun(edge) 178 self.cumulative_weights.append(self.total_weight) 179 if self.total_weight == 0: 180 raise InvalidGenerator() 181 182 def get(self): 183 r = random.random() * self.total_weight 184 index = bisect.bisect(self.cumulative_weights, r) 185 return self.net._edges[index] 186 187 def write_weights(self, fname): 188 # normalize to [0,100] 189 normalizer = 100.0 / max(1, max(map(self.weight_fun, self.net._edges))) 190 weights = [(self.weight_fun(e) * normalizer, e.getID()) for e in self.net.getEdges()] 191 weights.sort(reverse=True) 192 with open(fname, 'w+') as f: 193 f.write('<edgedata>\n') 194 f.write(' <interval begin="0" end="10">\n') 195 for weight, edgeID in weights: 196 f.write(' <edge id="%s" value="%0.2f"/>\n' % 197 (edgeID, weight)) 198 f.write(' </interval>\n') 199 f.write('</edgedata>\n') 200 201 202class RandomTripGenerator: 203 204 def __init__(self, source_generator, sink_generator, via_generator, intermediate, pedestrians): 205 self.source_generator = source_generator 206 self.sink_generator = sink_generator 207 self.via_generator = via_generator 208 self.intermediate = intermediate 209 self.pedestrians = pedestrians 210 211 def get_trip(self, min_distance, max_distance, maxtries=100): 212 for i in range(maxtries): 213 source_edge = self.source_generator.get() 214 intermediate = [self.via_generator.get() 215 for i in range(self.intermediate)] 216 sink_edge = self.sink_generator.get() 217 if self.pedestrians: 218 destCoord = sink_edge.getFromNode().getCoord() 219 else: 220 destCoord = sink_edge.getToNode().getCoord() 221 222 coords = ([source_edge.getFromNode().getCoord()] + 223 [e.getFromNode().getCoord() for e in intermediate] + 224 [destCoord]) 225 distance = sum([euclidean(p, q) 226 for p, q in zip(coords[:-1], coords[1:])]) 227 if distance >= min_distance and (max_distance is None or distance < max_distance): 228 return source_edge, sink_edge, intermediate 229 raise Exception("no trip found after %s tries" % maxtries) 230 231 232def get_prob_fun(options, fringe_bonus, fringe_forbidden): 233 # fringe_bonus None generates intermediate way points 234 def edge_probability(edge): 235 if options.vclass and not edge.allows(options.vclass): 236 return 0 # not allowed 237 if fringe_bonus is None and edge.is_fringe() and not options.pedestrians: 238 return 0 # not suitable as intermediate way point 239 if (fringe_forbidden is not None and edge.is_fringe(getattr(edge, fringe_forbidden)) and 240 not options.pedestrians and 241 (options.allow_fringe_min_length is None or edge.getLength() < options.allow_fringe_min_length)): 242 return 0 # the wrong kind of fringe 243 prob = 1 244 if options.length: 245 prob *= edge.getLength() 246 if options.lanes: 247 prob *= edge.getLaneNumber() 248 prob *= (edge.getSpeed() ** options.speed_exponent) 249 if (options.fringe_factor != 1.0 and 250 not options.pedestrians and 251 fringe_bonus is not None and 252 edge.getSpeed() > options.fringe_threshold and 253 edge.is_fringe(getattr(edge, fringe_bonus))): 254 prob *= options.fringe_factor 255 if options.edgeParam is not None: 256 prob *= float(edge.getParam(options.edgeParam, 1.0)) 257 return prob 258 return edge_probability 259 260 261class LoadedProps: 262 263 def __init__(self, fname): 264 self.weights = defaultdict(lambda: 0) 265 for edge in sumolib.output.parse_fast(fname, 'edge', ['id', 'value']): 266 self.weights[edge.id] = float(edge.value) 267 268 def __call__(self, edge): 269 return self.weights[edge.getID()] 270 271 272def buildTripGenerator(net, options): 273 try: 274 forbidden_source_fringe = None if options.allow_fringe else "_outgoing" 275 forbidden_sink_fringe = None if options.allow_fringe else "_incoming" 276 source_generator = RandomEdgeGenerator( 277 net, get_prob_fun(options, "_incoming", forbidden_source_fringe)) 278 sink_generator = RandomEdgeGenerator( 279 net, get_prob_fun(options, "_outgoing", forbidden_sink_fringe)) 280 if options.weightsprefix: 281 if os.path.isfile(options.weightsprefix + SOURCE_SUFFIX): 282 source_generator = RandomEdgeGenerator( 283 net, LoadedProps(options.weightsprefix + SOURCE_SUFFIX)) 284 if os.path.isfile(options.weightsprefix + SINK_SUFFIX): 285 sink_generator = RandomEdgeGenerator( 286 net, LoadedProps(options.weightsprefix + SINK_SUFFIX)) 287 except InvalidGenerator: 288 print("Error: no valid edges for generating source or destination. Try using option --allow-fringe", 289 file=sys.stderr) 290 return None 291 292 try: 293 via_generator = RandomEdgeGenerator( 294 net, get_prob_fun(options, None, None)) 295 if options.weightsprefix and os.path.isfile(options.weightsprefix + VIA_SUFFIX): 296 via_generator = RandomEdgeGenerator( 297 net, LoadedProps(options.weightsprefix + VIA_SUFFIX)) 298 except InvalidGenerator: 299 if options.intermediate > 0: 300 print( 301 "Error: no valid edges for generating intermediate points", file=sys.stderr) 302 return None 303 else: 304 via_generator = None 305 306 return RandomTripGenerator( 307 source_generator, sink_generator, via_generator, options.intermediate, options.pedestrians) 308 309 310def is_walk_attribute(attr): 311 for cand in ['arrivalPos', 'speed=', 'duration=', 'busStop=']: 312 if cand in attr: 313 return True 314 return False 315 316 317def is_persontrip_attribute(attr): 318 for cand in ['vTypes', 'modes']: 319 if cand in attr: 320 return True 321 return False 322 323 324def is_person_attribute(attr): 325 for cand in ['departPos', 'type']: 326 if cand in attr: 327 return True 328 return False 329 330 331def is_vehicle_attribute(attr): 332 for cand in ['depart', 'arrival', 'line', 'Number', 'type']: 333 if cand in attr: 334 return True 335 return False 336 337 338def split_trip_attributes(tripattrs, pedestrians, hasType): 339 # handle attribute values with a space 340 # assume that no attribute value includes an '=' sign 341 allattrs = [] 342 for a in tripattrs.split(): 343 if "=" in a: 344 allattrs.append(a) 345 else: 346 if len(allattrs) == 0: 347 print("Warning: invalid trip-attribute '%s'" % a) 348 else: 349 allattrs[-1] += ' ' + a 350 351 # figure out which of the tripattrs belong to the <person> or <vehicle>, 352 # which belong to the <vType> and which belong to the <walk> or <persontrip> 353 vehicleattrs = [] 354 personattrs = [] 355 vtypeattrs = [] 356 otherattrs = [] 357 for a in allattrs: 358 if pedestrians: 359 if is_walk_attribute(a) or is_persontrip_attribute(a): 360 otherattrs.append(a) 361 elif is_person_attribute(a): 362 personattrs.append(a) 363 else: 364 vtypeattrs.append(a) 365 else: 366 if is_vehicle_attribute(a): 367 vehicleattrs.append(a) 368 else: 369 vtypeattrs.append(a) 370 371 if not hasType: 372 if pedestrians: 373 personattrs += vtypeattrs 374 else: 375 vehicleattrs += vtypeattrs 376 vtypeattrs = [] 377 378 return (prependSpace(' '.join(vtypeattrs)), 379 prependSpace(' '.join(vehicleattrs)), 380 prependSpace(' '.join(personattrs)), 381 prependSpace(' '.join(otherattrs))) 382 383 384def prependSpace(s): 385 if len(s) == 0 or s[0] == " ": 386 return s 387 else: 388 return " " + s 389 390 391def main(options): 392 if options.seed: 393 random.seed(options.seed) 394 395 net = sumolib.net.readNet(options.netfile) 396 if options.min_distance > net.getBBoxDiameter() * (options.intermediate + 1): 397 options.intermediate = int( 398 math.ceil(options.min_distance / net.getBBoxDiameter())) - 1 399 print(("Warning: setting number of intermediate waypoints to %s to achieve a minimum trip length of " + 400 "%s in a network with diameter %.2f.") % ( 401 options.intermediate, options.min_distance, net.getBBoxDiameter())) 402 403 trip_generator = buildTripGenerator(net, options) 404 idx = 0 405 406 vtypeattrs, options.tripattrs, personattrs, otherattrs = split_trip_attributes( 407 options.tripattrs, options.pedestrians, options.vehicle_class) 408 409 vias = {} 410 411 def generate_one(idx): 412 label = "%s%s" % (options.tripprefix, idx) 413 try: 414 source_edge, sink_edge, intermediate = trip_generator.get_trip( 415 options.min_distance, options.max_distance, options.maxtries) 416 combined_attrs = options.tripattrs 417 if options.fringeattrs and source_edge.is_fringe(source_edge._incoming): 418 combined_attrs += " " + options.fringeattrs 419 via = "" 420 if len(intermediate) > 0: 421 via = ' via="%s" ' % ' '.join( 422 [e.getID() for e in intermediate]) 423 if options.validate: 424 vias[label] = via 425 if options.pedestrians: 426 fouttrips.write( 427 ' <person id="%s" depart="%.2f"%s>\n' % (label, depart, personattrs)) 428 if options.persontrips: 429 fouttrips.write( 430 ' <personTrip from="%s" to="%s"%s/>\n' % ( 431 source_edge.getID(), sink_edge.getID(), otherattrs)) 432 else: 433 fouttrips.write( 434 ' <walk from="%s" to="%s"%s/>\n' % (source_edge.getID(), sink_edge.getID(), otherattrs)) 435 fouttrips.write(' </person>\n') 436 elif options.flows > 0: 437 to = '' if options.jtrrouter else ' to="%s"' % sink_edge.getID() 438 if options.binomial: 439 for j in range(options.binomial): 440 fouttrips.write((' <flow id="%s#%s" begin="%s" end="%s" probability="%s" ' + 441 'from="%s"%s%s%s/>\n') % ( 442 label, j, options.begin, options.end, 1.0 / options.period / options.binomial, 443 source_edge.getID(), to, via, combined_attrs)) 444 else: 445 fouttrips.write((' <flow id="%s" begin="%s" end="%s" period="%s" from="%s"%s%s%s/>\n') % ( 446 label, options.begin, options.end, options.period * options.flows, source_edge.getID(), 447 to, via, combined_attrs)) 448 else: 449 fouttrips.write(' <trip id="%s" depart="%.2f" from="%s" to="%s"%s%s/>\n' % ( 450 label, depart, source_edge.getID(), sink_edge.getID(), via, combined_attrs)) 451 except Exception as exc: 452 print(exc, file=sys.stderr) 453 return idx + 1 454 455 with open(options.tripfile, 'w') as fouttrips: 456 sumolib.writeXMLHeader(fouttrips, "$Id$", "routes") # noqa 457 if options.vehicle_class: 458 fouttrips.write(' <vType id="%s" vClass="%s"%s/>\n' % 459 (options.vtypeID, options.vehicle_class, vtypeattrs)) 460 options.tripattrs += ' type="%s"' % options.vtypeID 461 personattrs += ' type="%s"' % options.vtypeID 462 depart = options.begin 463 if trip_generator: 464 if options.flows == 0: 465 while depart < options.end: 466 if options.binomial is None: 467 # generate with constant spacing 468 idx = generate_one(idx) 469 depart += options.period 470 else: 471 # draw n times from a Bernoulli distribution 472 # for an average arrival rate of 1 / period 473 prob = 1.0 / options.period / options.binomial 474 for i in range(options.binomial): 475 if random.random() < prob: 476 idx = generate_one(idx) 477 depart += 1 478 else: 479 for i in range(options.flows): 480 idx = generate_one(idx) 481 482 fouttrips.write("</routes>\n") 483 484 # call duarouter for routes or validated trips 485 args = [DUAROUTER, '-n', options.netfile, '-r', options.tripfile, '--ignore-errors', 486 '--begin', str(options.begin), '--end', str(options.end), '--no-step-log', '--no-warnings'] 487 if options.additional is not None: 488 args += ['--additional-files', options.additional] 489 if options.carWalkMode is not None: 490 args += ['--persontrip.transfer.car-walk', options.carWalkMode] 491 if options.walkfactor is not None: 492 args += ['--persontrip.walkfactor', options.walkfactor] 493 if options.routefile: 494 args2 = args + ['-o', options.routefile] 495 print("calling ", " ".join(args2)) 496 subprocess.call(args2) 497 498 if options.validate: 499 # write to temporary file because the input is read incrementally 500 tmpTrips = options.tripfile + ".tmp" 501 args2 = args + ['-o', tmpTrips, '--write-trips'] 502 print("calling ", " ".join(args2)) 503 subprocess.call(args2) 504 os.remove(options.tripfile) # on windows, rename does not overwrite 505 os.rename(tmpTrips, options.tripfile) 506 507 if options.weights_outprefix: 508 trip_generator.source_generator.write_weights( 509 options.weights_outprefix + SOURCE_SUFFIX) 510 trip_generator.sink_generator.write_weights( 511 options.weights_outprefix + SINK_SUFFIX) 512 if trip_generator.via_generator: 513 trip_generator.via_generator.write_weights( 514 options.weights_outprefix + VIA_SUFFIX) 515 516 # return wether trips could be generated as requested 517 return trip_generator is not None 518 519 520if __name__ == "__main__": 521 if not main(get_options()): 522 sys.exit(1) 523