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