From 235325c814545c089678ee67c8ab5f8ed32912fd Mon Sep 17 00:00:00 2001 From: Jakob Erdmann Date: Tue, 19 Mar 2024 19:36:19 +0100 Subject: [PATCH] first version of #5320 --- tools/ptlines2flows.py | 172 +++++++++++++++++++++++++++++++++++------ 1 file changed, 148 insertions(+), 24 deletions(-) diff --git a/tools/ptlines2flows.py b/tools/ptlines2flows.py index c58573c40a8..da0490b03d4 100755 --- a/tools/ptlines2flows.py +++ b/tools/ptlines2flows.py @@ -23,10 +23,12 @@ import subprocess import collections import random +import math import sumolib from sumolib.xml import quoteattr from sumolib.options import ArgumentParser +from sumolib import geomhelper def get_options(args=None): @@ -51,12 +53,16 @@ def get_options(args=None): "if none is specified in the ptlines file")) ap.add_option("-b", "--begin", type=ap.time, default=0, help="start time") ap.add_option("-e", "--end", type=ap.time, default=3600, help="end time") - ap.add_option("-j", "--jump-duration", type=ap.time, default=300, - help="The time to add for each missing (when joining opposite lines)") + ap.add_option("-j", "--jump-duration", type=ap.time, default=180, dest="jumpDuration", + help="The time to add for each missing stop (when joining opposite lines)") ap.add_option("-T", "--turnaround-duration", type=ap.time, default=300, help="The extra stopping time at terminal stops") - ap.add_option("--no-join", default=False, action="store_true", dest='noJoin', - help="Do not join opposite lines at the terminals") + ap.add_option("--join", default=False, action="store_true", + help="Join opposite lines at the terminals") + ap.add_option("--join-threshold", default=100, type=float, dest='joinThreshold', + help="maximum distance for terminal stops when joining lines") + ap.add_option("--multistart", default=False, action="store_true", + help="Insert multiple vehicles per line at different offsets along the route to avoid warmup") ap.add_option("--min-stops", type=int, default=2, help="only import lines with at least this number of stops") ap.add_option("-f", "--flow-attributes", dest="flowattrs", default="", help="additional flow attributes") ap.add_option("--use-osm-routes", default=False, action="store_true", dest='osmRoutes', help="use osm routes") @@ -111,8 +117,8 @@ def __init__(self, ref, name, completeness, missingBefore, missingAfter, self.ref = ref self.name = name self.completeness = completeness - self.missingBeore = missingBefore - self.missingAfter = missingAfter + self.missingBefore = int(missingBefore) if missingBefore is not None else 0 + self.missingAfter = int(missingAfter) if missingAfter is not None else 0 self.fromEdge = fromEdge self.toEdge = toEdge self.period = period @@ -122,6 +128,10 @@ def __init__(self, ref, name, completeness, missingBefore, missingAfter, self.depart = depart self.stop_ids = stop_ids self.vias = vias + # stop indices that need special handling + self.jumps = {} # stopIndex -> duration + self.terminalIndices = [] + self.repeat = None def writeTypes(fout, prefix, options): @@ -173,7 +183,6 @@ def createTrips(options): tripList = [] # ids trpMap = {} # ids->PTLine - linePairs = collections.defaultdict(list) departTimes = [options.begin for line in sumolib.output.parse_fast(options.ptlines, 'ptLine', ['id'])] if options.randomBegin: @@ -284,9 +293,8 @@ def createTrips(options): fromEdge = getStopEdge(stopsLanes, stop_ids[0]) toEdge = getStopEdge(stopsLanes, stop_ids[-1]) - linePairs[lineRefOrig].append(lineRef) - missingBefore = 0 if line.completeness == 1 else line.getAttributeSecure(line.missingBefore) - missingAfter = 0 if line.completeness == 1 else line.getAttributeSecure(line.missingAfter) + missingBefore = 0 if line.completeness == 1 else line.missingBefore + missingAfter = 0 if line.completeness == 1 else line.missingAfter typeID = options.vtypeprefix + line.type trpMap[tripID] = PTLine(lineRef, line.attr_name, line.completeness, missingBefore, missingAfter, @@ -302,6 +310,8 @@ def createTrips(options): numLines += 1 numStops += len(stop_ids) + if options.join: + joinTrips(options, tripList, trpMap) writeTrips(options, tripList, trpMap) if options.verbose: print("Imported %s lines with %s stops and skipped %s lines" % (numLines, numStops, numSkipped)) @@ -311,6 +321,79 @@ def createTrips(options): return trpMap, stopNames +def joinTrips(options, tripList, trpMap): + # join opposite pairs of trips + linePairs = collections.defaultdict(list) + for tripID, ptl in trpMap.items(): + linePairs[ptl.refOrig].append(tripID) + + for refOrig, tripIDs in linePairs.items(): + if len(tripIDs) > 2: + sys.stderr.write("Warning: Cannot join line '%s' with more %s trips" % (refOrig, len(tripIDs))) + elif len(tripIDs) == 2: + net = getNet(options) + ptl1 = trpMap[tripIDs[0]] + ptl2 = trpMap[tripIDs[1]] + join1 = distCheck(options, refOrig, ptl1.toEdge, ptl2.fromEdge) + join2 = distCheck(options, refOrig, ptl1.fromEdge, ptl2.toEdge) + if not join1 and not join2: + continue + ptl1.completeness = str(0.5 * (float(ptl1.completeness) + float(ptl2.completeness))) + ptl2.completeness = ptl1.completeness + ptl1.ref = ptl1.refOrig + ptl2.ref = ptl2.refOrig + + from1 = net.getEdge(ptl1.fromEdge) + from2 = net.getEdge(ptl2.fromEdge) + to1 = net.getEdge(ptl1.toEdge) + to2 = net.getEdge(ptl2.toEdge) + missingPart1 = ptl1.missingAfter + ptl2.missingBefore + missingPart2 = ptl2.missingAfter + ptl1.missingBefore + + if join1: + # append ptl1 after ptl2 + tripList.remove(tripIDs[1]) + if missingPart1 != 0 or to1.getBidi() != from2.getID(): + ptl1.jumps[len(ptl1.stop_ids) - 1] = missingPart1 * options.jumpDuration + ptl1.terminalIndices.append(len(ptl1.stop_ids) - 1) + ptl1.stop_ids += ptl2.stop_ids + ptl1.vias += ptl2.vias + ptl1.toEdge = ptl2.toEdge + + if join2: + # line forms a full circle so that vehicles can stay in the simulation continously. + # We have to compute the appropriate number of vehicles and then adapt the end time + if missingPart2 != 0 or to2.getBidi() != from1.getID(): + ptl1.jumps[len(ptl1.stop_ids) - 1] = missingPart2 * options.jumpDuration + ptl1.terminalIndices.append(len(ptl1.stop_ids) - 1) + + + elif join2: + # only append ptl1 after ptl2 + tripList.remove(tripIDs[0]) + if missingPart2 != 0 or to2.getBidi() != from1.getID(): + ptl2.jumps[len(ptl2.stop_ids) - 1] = missingPart2 * options.jumpDuration + ptl2.terminalIndices.append(len(ptl2.stop_ids) - 1) + ptl2.stop_ids += ptl1.stop_ids + ptl2.vias += ptl1.vias + ptl2.toEdge = ptl1.toEdge + + +def distCheck(options, refOrig, eID1, eID2): + net = getNet(options) + shape1 = net.getEdge(eID1).getShape(True) + shape2 = net.getEdge(eID2).getShape(True) + minDist = options.joinThreshold + 1 + for p in shape1: + minDist = min(minDist, geomhelper.distancePointToPolygon(p, shape2)) + if minDist > options.joinThreshold: + sys.stderr.write("Warning: Cannot join line '%s' at edges '%s' and '%s' with distance %s" % ( + refOrig, eID1, eID2, minDist)) + return False + else: + return True + + def writeTrips(options, tripList, trpMap): with codecs.open(options.trips, 'w', encoding="UTF8") as fouttrips: sumolib.writeXMLHeader( @@ -324,9 +407,13 @@ def writeTrips(options, tripList, trpMap): (' \n') % ( tripID, ptl.typeID, ptl.depart, ptl.fromEdge, ptl.toEdge, ptl.vias)) - for stop in ptl.stop_ids: + for i, stop in enumerate(ptl.stop_ids): dur = options.stopduration + options.stopdurationSlack - fouttrips.write(' \n' % (stop, dur)) + if i in ptl.jumps: + jump = ' jump="%s"' % ptl.jumps[i] + else: + jump = "" + fouttrips.write(' \n' % (stop, dur, jump)) fouttrips.write(' \n') fouttrips.write("\n") @@ -369,6 +456,9 @@ def createRoutes(options, trpMap, stopNames): if not options.novtypes: writeTypes(foutflows, options.vtypeprefix, None) collections.defaultdict(int) + routeDurations = {} + routeSize = {} + flow_duration = options.end - options.begin for vehicle in sumolib.output.parse(options.routes, 'vehicle'): id = vehicle.id ptline = trpMap[id] @@ -378,6 +468,7 @@ def createRoutes(options, trpMap, stopNames): edges = vehicle.route[0].edges else: edges = vehicle.routeDistribution[0].route[1].edges + routeSize[flowID] = len(edges.split()) except BaseException: if options.ignoreErrors: sys.stderr.write("Warning: Could not parse edges for vehicle '%s'\n" % id) @@ -389,38 +480,71 @@ def createRoutes(options, trpMap, stopNames): parking = ' parking="true"' if vehicle.type == "bus" and options.busparking else '' stops = vehicle.stop color = ' color="%s"' % ptline.color if ptline.color is not None else "" - foutflows.write(' \n' % (flowID, color, edges)) + repeat = "" + if len(ptline.terminalIndices) == 2 and stops: + lastBusStop = stops[-1].busStop + lastUntil = stopsUntil.get((id, lastBusStop)) + if lastUntil is not None: + numRepeats = math.ceil(flow_duration / (lastUntil[-1] - actualDepart[id])) + if numRepeats > 1: + repeat = ' repeat="%s"' % numRepeats + + foutflows.write(' \n' % (flowID, color, edges, repeat)) if vehicle.stop is not None: for stop in stops: if (id, stop.busStop) in stopsUntil: until = stopsUntil[(id, stop.busStop)] stopname = ' ' % stopNames[stop.busStop] if stop.busStop in stopNames else '' untilZeroBased = until[0] - actualDepart[id] + if stop.jump is not None: + jump = ' jump="%s"' % stop.jump + else: + jump = "" if len(until) > 1: stopsUntil[(id, stop.busStop)] = until[1:] foutflows.write( - ' %s\n' % ( - stop.busStop, options.stopduration, ft(untilZeroBased), parking, stopname)) + ' %s\n' % ( + stop.busStop, options.stopduration, ft(untilZeroBased), parking, jump, stopname)) + routeDurations[flowID] = untilZeroBased else: sys.stderr.write("Warning: Missing stop '%s' for flow '%s'\n" % (stop.busStop, id)) else: sys.stderr.write("Warning: No stops for flow '%s'\n" % id) foutflows.write(' \n') - flow_duration = options.end - options.begin for vehID, flowID, lineRef, type, begin in flows: ptline = trpMap[vehID] - foutflows.write(' \n' % ( - flowID, type, flowID, ft(begin), ft(begin + flow_duration), - int(float(ptline.period)), ptline.ref, options.flowattrs)) - if ptline.name is not None: - foutflows.write(' \n' % quoteattr(ptline.name)) - if ptline.completeness is not None: - foutflows.write(' \n' % quoteattr(ptline.completeness)) - foutflows.write(' \n') + number = None + if flowID in routeDurations: + number = max(1, int(routeDurations[flowID] / float(ptline.period))) + if options.multistart and len(ptline.terminalIndices) == 2: + # vehicles stay in a continuous loop. We create a fixed number of vehicles with repeating routes + for i in range(number): + departEdge = int(i * routeSize[flowID] / float(number)) # this is a hack since edges could have very different lengths + foutflows.write(' \n' % ( + flowID, i, type, flowID, ft(begin), departEdge, ptline.ref, options.flowattrs)) + writeParams(foutflows, ptline) + foutflows.write(' \n') + foutflows.write('\n') + + else: + end = ' end="%s"' % ft(begin + flow_duration) + if len(ptline.terminalIndices) == 2 and number is not None: + end = ' number="%s"' % number + foutflows.write(' \n' % ( + flowID, type, flowID, ft(begin), end, + int(float(ptline.period)), ptline.ref, options.flowattrs)) + writeParams(foutflows, ptline) + foutflows.write(' \n') foutflows.write('\n') print("done.") +def writeParams(foutflows, ptline): + if ptline.name is not None: + foutflows.write(' \n' % quoteattr(ptline.name)) + if ptline.completeness is not None: + foutflows.write(' \n' % quoteattr(ptline.completeness)) + def main(options): if options.seed: