Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/generateRailSignalConstraints.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 generateRailSignalConstraints.py
15
# @author Jakob Erdmann
16
# @date 2020-08-31
17
18
"""
19
Generate railSignalConstrains definitions that enforce a loaded rail schedule
20
(Zuglenkung)
21
22
Different types of constraints are generated in different cases:
23
24
1. <predecessor>
25
When two vehicles stop subsequently at the same busStop (trainStop) and they reach that stop
26
via different routes, the switch where both routes merge is identified and a
27
constraint is created for the rail signals that guard this merging switch:
28
The vehicle B that arrives at the stop later, must wait (at its signal Y)
29
for the vehicle A that arrives first (to pass its respective signal X)
30
This uses the 'arrival' attribute of the vehicle stops
31
32
A complication arises if the signal of the first vehicle is passed by other
33
trains which are en route to another stop. This makes it necessary to record a
34
larger number of passing vehicles within the simulation (controlled by the
35
limit attribute). The script attempts to determine the necessary limit value by
36
identifying all vehicles that pass the signal X en route to other stops between
37
the time A and B reach their respective signals (counting backwards from the
38
next stop based on "arrival". To account for delays the
39
options --delay and --limit can be used to override the limit values
40
41
A more complicated case arises when the next stop after the merging switch differs for
42
both trains even though their routes are the same. In this case the ordering of
43
the trains must be deduced from their next common stop and the algorithm adds
44
a "virtual" intermediateStop directly after the switch to "normalize" the input.
45
46
2. <insertionPredecessor>
47
Whenever a vehicle B departs at a stop (assumed to coincide with the "until"
48
attribute of its first stop), the prior train A that leaves this stop is
49
identified (also based on "until"). Then a constraint is created that prevents
50
insertion of B until train A has passed the next signal that lies beyond the
51
stop.
52
53
These constraints are also needed in the context of parking-stops because these
54
have the potential to alter train ordering:
55
56
If vehicle A has a parking stop with 'ended' time and vehicle B has a
57
parking stop at the same location without 'ended' (only an 'until' time),
58
an insertionPredecessor constraint is created for B to ensure that A leaves
59
first. This is because availability of an 'ended' value implies that the even is
60
in the past whereas the lack of the value indicates that the stop is still in
61
the future.
62
63
In the case where an intermediateStop (see above) is also a parking stop, an
64
insertionPredecessor constraint is added if the parking vehicle is scheduled to
65
go second.
66
67
3. <foeInsertion>
68
Whenever a vehicle A departs at a stop (assumed to coincide with the "until"
69
attribute of its first stop), the latter train B that enters this stop is
70
identified (also based on "until"). Then a constraint is created that prevents
71
B from entering the section with the stop until A has passed the next signal that lies beyond the
72
stop.
73
74
4. <insertionOrder>
75
Whenever two vehicles depart at the same stop and their until/ended times at that stop
76
are in a different order from their departure times, an insertionOrder constraint
77
is added the delays insertion to achieve the desired order.
78
This may happen if departure times reflect the schedule
79
but until/ended times reflect post-facto timing (see below)
80
81
5. <bidiPredecessor>
82
Whenever two trains approach the same track section from different directions, a bidiPredecessor
83
constraint may optionally be generated to enforce the order of entering that section based on the
84
stop arrival times that follow the section on the respective side.
85
(stop arrival times must be corrected for the estimated travel time between the end of the conflict section
86
and the stop)
87
88
== Inconsistencies ==
89
90
Inconsistent constraints may arise from inconsistent input and cause simulation
91
deadlock. To avoid this, the option --abort-unordered can be used to avoid
92
generating constraints that are likely to be inconsistent.
93
When the option is set the ordering of vehicles is cross-checked with regard to
94
arrival and until times:
95
96
Given two vehicles A and B which stop at the same location, if A arrives at the
97
stop later than B, but A also leaves earlier than B, then B is "overtaken" by A.
98
All subsequent stops of B are marked as invalid and will not
99
participate in constraint generation. If the stop where overtaking took place
100
doesn't have a 'started' value (which implies that the original schedule is
101
inconsistent), then this stop is also marked as invalid.
102
103
If two vehicles have a 'parking'-stop with the same 'until' time at the same
104
location, their stops will also be marked as invalid since the simulation cannot
105
enforce an order in this case (and local desired order is ambiguous).
106
107
Another kind of inconsistency is indicated by 'ended' times that lie ahead of
108
the 'until' time of the respective stop by a significant margin (--.
109
This situation may correspond to the actions of
110
a real-life dispatcher. In such a case, the must not be constraint any further
111
since it is no longer running according to the schedule.
112
113
== Post-Facto Stop Timings ==
114
115
When simulating the past (i.e. to predict the future), additional timing data
116
besides the scheduled arrival and until times may be available and included in
117
the 'started' and 'ended' attributes for each stop.
118
They can be used to detect changes in train order that occurred during the actual
119
train operation and which must be taken into account during constraint
120
generation to avoid deadlock.
121
If train A has 'started' information for a
122
stop while train B has not, this implies that A has reached the stop ahead of B.
123
Likewise, both trains may have 'started' information but in the reverse order
124
compared to the schedule.
125
For all stops with complete started,ended information, those times can be used
126
as an updated schedule (replacing arrival and until). However, if an order
127
reversal was detected for a train, no constraints based on the old schedule
128
should be generated anymore (stops are ignored after started,ended information ends)
129
130
131
== Further Options ==
132
If constraints shall be modified during the simulation (traci.trafficlight.swapConstraints)
133
it may be useful to add additional constraints which would otherwise be
134
redundant. This can be accomplished by setting option --redundant with a time
135
range. When set, trains that follow a constrained train within the given time
136
range (and which would normally be constrained implicitly by their leading
137
train) will also receive a constraint. In this case option --limit must be used
138
to ensure that all constraint foe vehicles are recorded during the simulation.
139
140
"""
141
142
from __future__ import absolute_import
143
from __future__ import print_function
144
145
import os
146
import sys
147
import subprocess
148
from collections import defaultdict
149
from operator import itemgetter
150
import copy
151
152
if 'SUMO_HOME' in os.environ:
153
sys.path.append(os.path.join(os.environ['SUMO_HOME'], 'tools'))
154
import sumolib # noqa
155
from sumolib.miscutils import parseTime, parseBool, humanReadableTime # noqa
156
157
DUAROUTER = sumolib.checkBinary('duarouter')
158
159
160
def get_options(args=None):
161
parser = sumolib.options.ArgumentParser(description="Sample routes to match counts")
162
parser.add_argument("-n", "--net-file", category="input", dest="netFile",
163
help="Input network file")
164
parser.add_argument("-a", "--additional-file", category="input", dest="addFile",
165
help="Input additional file (busStops)")
166
parser.add_argument("-t", "--trip-file", category="input", dest="tripFile",
167
help="Input trip file (will be processed into a route file)")
168
parser.add_argument("-r", "--route-file", category="input", dest="routeFile",
169
help="Input route file (must contain routed vehicles rather than trips)")
170
parser.add_argument("-o", "--output-file", category="output", dest="out", default="constraints.add.xml",
171
help="Output additional file")
172
parser.add_argument("-b", "--begin", category="time", default="0",
173
help="ignore vehicles departing before the given begin time (seconds or H:M:S)")
174
parser.add_argument("--until-from-duration", action="store_true", default=False, dest="untilFromDuration",
175
category="time",
176
help="Use stop arrival+duration instead of 'until' to compute insertion constraints")
177
parser.add_argument("-d", "--delay", category="time", default="0",
178
help="Assume given maximum delay when computing the number of intermediate vehicles " +
179
"that pass a given signal (for setting limit)")
180
parser.add_argument("-l", "--limit", category="processing", type=int, default=0,
181
help="Increases the limit value for tracking passed vehicles by the given amount")
182
parser.add_argument("--abort-unordered", dest="abortUnordered", action="store_true", default=False,
183
category="processing",
184
help="Abort generation of constraints for a stop "
185
"once the ordering of vehicles by 'arrival' differs from the ordering by 'until'")
186
parser.add_argument("--abort-unordered.keep-actual",
187
dest="abortUnorderedKeepActual", action="store_true", default=False,
188
category="processing",
189
help="Keep constraints for a stop with 'started' and 'ended' value even after "
190
"the ordering of vehicles by 'arrival' differs from the ordering by 'until'")
191
parser.add_argument("--premature-threshold", category="processing", default=600, dest="prematureThreshold",
192
help="Ignore schedule if a train leaves a station ahead of schedule by " +
193
"more than the threshold value")
194
parser.add_argument("--write-inactive", dest="writeInactive", action="store_true", default=False,
195
category="processing",
196
help="Export aborted constraints as inactive")
197
parser.add_argument("--all-inactive", dest="allInactive", action="store_true", default=False,
198
category="processing",
199
help="Export all constraints as inactive")
200
parser.add_argument("-p", "--ignore-parking", dest="ignoreParking", action="store_true", default=False,
201
category="processing",
202
help="Ignore unordered timing if the vehicle which arrives first is parking")
203
parser.add_argument("-P", "--skip-parking", dest="skipParking", action="store_true", default=False,
204
category="processing",
205
help="Do not generate constraints for a vehicle that parks at the next stop")
206
parser.add_argument("--redundant", category="processing", default=-1,
207
help=("Add redundant constraint within given time range " +
208
"(reduces impact of modifying constraints at runtime)"))
209
parser.add_argument("--bidi-max-range", category="processing", dest="bidiMaxRange", type=float, default=1,
210
help="Find bidiStops on sequential edges within the given range in m")
211
parser.add_argument("--bidi-conflicts", dest="bidiConflicts", action="store_true", default=False,
212
category="processing",
213
help="Write bidiPredecessor constraints")
214
parser.add_argument("--comment.line", category="processing", action="store_true", dest="commentLine", default=False,
215
help="add lines of involved trains in comment")
216
parser.add_argument("--comment.id", category="processing", action="store_true", dest="commentId", default=False,
217
help="add ids of involved trains in comment (when different from tripId)")
218
parser.add_argument("--comment.switch", action="store_true", dest="commentSwitch", default=False,
219
category="processing",
220
help="add id of the merging switch that prompted the constraint")
221
parser.add_argument("--comment.stop", category="processing", action="store_true", dest="commentStop", default=False,
222
help="add busStop id that was used to determine the train ordering for the constraint")
223
parser.add_argument("--comment.time", category="processing", action="store_true", dest="commentTime", default=False,
224
help="add timing information for the constraint")
225
parser.add_argument("--comment.all", category="processing", action="store_true", dest="commentAll", default=False,
226
help="add all comments")
227
parser.add_argument("--params", category="processing", action="store_true", dest="commentParams", default=False,
228
help="stores comments as params")
229
parser.add_argument("-v", "--verbose", category="processing", action="store_true", default=False,
230
help="tell me what you are doing")
231
parser.add_argument("--debug-switch", category="processing", dest="debugSwitch",
232
help="print debug information for the given merge-switch edge")
233
parser.add_argument("--debug-signal", category="processing", dest="debugSignal",
234
help="print debug information for the given signal id")
235
parser.add_argument("--debug-stop", category="processing", dest="debugStop",
236
help="print debug information for the given busStop id")
237
parser.add_argument("--debug-vehicle", category="processing", dest="debugVehicle",
238
help="print debug information for the given vehicle id")
239
parser.add_argument("--debug-foe-vehicle", dest="debugFoeVehicle",
240
help="print debug information for the given (foe) vehicle id")
241
parser.add_argument("--debug-edge", category="processing", dest="debugEdge",
242
help="print debug information for the given edge id")
243
244
options = parser.parse_args(args=args)
245
if (options.routeFile is None and options.tripFile is None) or options.netFile is None:
246
parser.print_help()
247
sys.exit()
248
249
if options.routeFile is None:
250
options.routeFile = options.tripFile + ".rou.xml"
251
args = [DUAROUTER, '-n', options.netFile,
252
'-r', options.tripFile,
253
'-a', options.addFile,
254
'-o', options.routeFile,
255
'--ignore-errors', '--no-step-log',
256
]
257
print("calling", " ".join(args))
258
sys.stdout.flush()
259
subprocess.call(args)
260
sys.stdout.flush()
261
262
if options.commentAll:
263
options.commentLine = True
264
options.commentId = True
265
options.commentSwitch = True
266
options.commentStop = True
267
options.commentTime = True
268
269
options.delay = parseTime(options.delay)
270
options.redundant = parseTime(options.redundant)
271
options.prematureThreshold = parseTime(options.prematureThreshold)
272
273
return options
274
275
276
def formatStopTimes(arrival, until, started, ended):
277
times = [arrival, until]
278
if started is not None:
279
times.append(started)
280
if ended is not None:
281
assert started is not None
282
times.append(ended)
283
return "(%s)" % ', '.join(map(humanReadableTime, times))
284
285
286
class Conflict:
287
def __init__(self, tripID, otherSignal, otherTripID, limit, line, otherLine,
288
vehID, otherVehID, conflictTime, switch, busStop, info,
289
active=True, tag=None, busStop2=None):
290
self.tripID = tripID
291
self.otherSignal = otherSignal
292
self.otherTripID = otherTripID
293
self.limit = limit
294
self.line = line
295
self.otherLine = otherLine
296
self.vehID = vehID
297
self.otherVehID = otherVehID
298
self.conflictTime = conflictTime
299
self.switch = switch
300
self.busStop = busStop
301
self.busStop2 = busStop2
302
self.info = info
303
self.active = active
304
self.tag = tag
305
306
307
def getTravelTime(net, edges):
308
"""compute free flow travel time on the given edges"""
309
result = 0
310
for edgeID in edges:
311
edge = net.getEdge(edgeID)
312
result += edge.getLength() / edge.getSpeed()
313
return result
314
315
316
def getTravelTimeToStop(net, e1end, edgesBefore, stop, excludeEnd):
317
"""compute free flow from the given edge up to the given stop"""
318
# e1end is the end of the conflict section
319
i = edgesBefore.index(e1end)
320
if excludeEnd:
321
i += 1
322
result = getTravelTime(net, edgesBefore[i:])
323
stopEdge = net.getEdge(edgesBefore[-1])
324
distAfterStop = stopEdge.getLength() - float(stop.endPos)
325
result -= distAfterStop / stopEdge.getSpeed()
326
return result
327
328
329
def getStopEdges(net, addFile):
330
"""find edge for each stopping place"""
331
stopEdges = {}
332
stopEnds = {}
333
for busstop in sumolib.xml.parse(addFile, 'busStop'):
334
stopEdge = sumolib._laneID2edgeID(busstop.lane)
335
stopEdges[busstop.id] = stopEdge
336
endPos = busstop.endPos
337
if endPos is None:
338
endPos = net.getEdge(stopEdge).getLength()
339
stopEnds[busstop.id] = endPos
340
341
print("read %s busStops on %s edges" % (len(stopEdges), len(set(stopEdges.values()))))
342
return stopEdges, stopEnds
343
344
345
def getNextStraight(edge, nextDict):
346
"""return successor edge if it is unique (excluding bidi)"""
347
succs = list(nextDict.keys())
348
if len(succs) == 1:
349
if succs[0] != edge.getBidi():
350
return succs[0]
351
elif len(succs) == 2:
352
if succs[0] == edge.getBidi():
353
return succs[1]
354
elif succs[1] == edge.getBidi():
355
return succs[0]
356
return None
357
358
359
def getBidiSequence(options, net, edge):
360
"""find continuous sequence of bidi edges upstream and downstream of the given edge within the given range.
361
The search in each direction is aborted when encountering a switch"""
362
result = []
363
if edge.getBidi() is None:
364
return result
365
result.append(edge.getBidi())
366
# downstream
367
remRange = options.bidiMaxRange
368
nextEdge = getNextStraight(edge, edge.getOutgoing())
369
while nextEdge is not None and nextEdge.getBidi() is not None and remRange > 0:
370
remRange -= nextEdge.getLength()
371
result.append(nextEdge.getBidi())
372
if edge.getID() == options.debugEdge:
373
print("forwardNext=%s remRange=%s" % (nextEdge.getID(), remRange))
374
nextEdge = getNextStraight(nextEdge, nextEdge.getOutgoing())
375
# upstream
376
remRange = options.bidiMaxRange
377
nextEdge = getNextStraight(edge, edge.getIncoming())
378
while nextEdge is not None and nextEdge.getBidi() is not None and remRange > 0:
379
remRange -= nextEdge.getLength()
380
result.append(nextEdge.getBidi())
381
if edge.getID() == options.debugEdge:
382
print("backwardNext=%s remRange=%s" % (nextEdge.getID(), remRange))
383
nextEdge = getNextStraight(nextEdge, nextEdge.getIncoming())
384
return result
385
386
387
def getBidiStops(options, net, stopEdges):
388
"""find bidi-stop(s) for each stop"""
389
# reverse stopEdges map (there may be more than one stop per edge)
390
edgeStops = defaultdict(list)
391
maxStopsPerEdge = 0
392
maxStopsEdge = None
393
for busStop, edgeID in sorted(stopEdges.items()):
394
edgeStops[edgeID].append(busStop)
395
nStops = len(edgeStops[edgeID])
396
if nStops > maxStopsPerEdge:
397
maxStopsPerEdge = nStops
398
maxStopsEdge = edgeID
399
400
bidiStops = defaultdict(list)
401
for busStop, edgeID in stopEdges.items():
402
edge = net.getEdge(edgeID)
403
for bidi in getBidiSequence(options, net, edge):
404
bidiID = bidi.getID()
405
if bidiID in edgeStops:
406
bidiStops[busStop] += edgeStops[bidiID]
407
# print("stop=%s bidiStops=%s" % (busStop, bidiStops[busStop]))
408
409
if len(bidiStops) > 0:
410
print("found %s bidi-stops (max stops per edge %s on %s)" % (len(bidiStops), maxStopsPerEdge, maxStopsEdge))
411
return bidiStops
412
413
414
def getStopRoutes(net, options, stopEdges, stopEnds, bidiStops):
415
"""parse routes and determine the list of edges between stops
416
return: setOfUniqueRoutes, busstopDict
417
"""
418
uniqueRoutes = defaultdict(list) # edges -> [vehID, vehID2, ..]
419
stopRoutes = defaultdict(list) # busStop -> [(edges, stopObj), ....]
420
stopRoutesBidi = defaultdict(list) # busStop -> [(edges, stopObj), ....]
421
vehicleStopRoutes = defaultdict(list) # vehID -> [(edges, stopObj), ....]
422
departTimes = {}
423
numRoutes = 0
424
numStops = 0
425
begin = parseTime(options.begin)
426
for vehicle in sumolib.xml.parse(options.routeFile, 'vehicle', heterogeneous=True):
427
depart = parseTime(vehicle.depart)
428
if depart is not None and depart < begin:
429
continue
430
departTimes[vehicle.id] = depart
431
numRoutes += 1
432
edges = tuple(vehicle.route[0].edges.split())
433
uniqueRoutes[edges].append(vehicle.id)
434
lastIndex = -1
435
routeIndex = 0
436
tripId = vehicle.id
437
if vehicle.param is not None:
438
for param in vehicle.param:
439
if param.key == "tripId":
440
tripId = param.value
441
line = vehicle.getAttributeSecure("line", "")
442
for stop in vehicle.stop:
443
numStops += 1
444
if stop.busStop is None:
445
if stop.edge is None:
446
stop.setAttribute("busStop", stop.lane)
447
stopEdges[stop.lane] = sumolib._laneID2edgeID(stop.lane)
448
stopEnds[stop.lane] = net.getLane(stop.lane).getLength()
449
else:
450
stop.setAttribute("busStop", stop.edge)
451
stopEdges[stop.edge] = stop.edge
452
stopEnds[stop.edge] = net.getEdge(stop.edge).getLength()
453
stopEdge = stopEdges[stop.busStop]
454
while edges[routeIndex] != stopEdge:
455
routeIndex += 1
456
assert routeIndex < len(edges)
457
edgesBefore = edges[lastIndex + 1: routeIndex + 1]
458
stop.setAttribute("prevTripId", tripId)
459
stop.setAttribute("prevLine", line)
460
stop.setAttribute("vehID", vehicle.id)
461
stop.setAttribute("endPos", stopEnds[stop.busStop])
462
tripId = stop.getAttributeSecure("tripId", tripId)
463
line = stop.getAttributeSecure("line", line)
464
stopRoutes[stop.busStop].append((edgesBefore, stop))
465
stopRoutesBidi[stop.busStop].append((edgesBefore, stop))
466
for bidiStop in bidiStops[stop.busStop]:
467
stopRoutesBidi[bidiStop].append((edgesBefore, stop))
468
vehicleStopRoutes[vehicle.id].append((edgesBefore, stop))
469
lastIndex = routeIndex
470
471
print("read %s routes (%s unique) and %s stops at %s busStops" % (
472
numRoutes, len(uniqueRoutes), numStops, len(stopRoutes)))
473
474
return uniqueRoutes, stopRoutes, stopRoutesBidi, vehicleStopRoutes, departTimes
475
476
477
def findMergingSwitches(options, uniqueRoutes, net):
478
"""find switches where routes merge and thus conflicts must be solved"""
479
predEdges = defaultdict(set)
480
predReversal = set()
481
for edges in uniqueRoutes.keys():
482
for i, edge in enumerate(edges):
483
if i > 0:
484
pred = edges[i - 1]
485
if net.getEdge(edge).getBidi() == net.getEdge(pred):
486
predReversal.add(edge)
487
predEdges[edge].add(pred)
488
489
mergeSwitches = set()
490
numReversals = 0
491
for edge in sorted(predEdges.keys()):
492
preds = predEdges[edge]
493
if len(preds) > 1:
494
reversalInfo = ""
495
if edge in predReversal:
496
numReversals += 1
497
reversalInfo = " (has reversal)"
498
if options.verbose:
499
print("mergingEdge=%s pred=%s%s" % (
500
edge, ','.join(sorted(preds)), reversalInfo))
501
mergeSwitches.add(edge)
502
503
if numReversals == 0:
504
reversalInfo = ""
505
else:
506
reversalInfo = " (including %s reversal-merges)" % numReversals
507
print("processed %s routes across %s edges with %s merging switches%s" % (
508
len(uniqueRoutes), len(predEdges), len(mergeSwitches), reversalInfo))
509
return mergeSwitches
510
511
512
def findStopsAfterMerge(net, stopRoutes, mergeSwitches):
513
"""find stops at the same busStop that come directly after a the same merge switch (no prior stop before
514
the switch). Returns filtered stopRoutes"""
515
switchRoutes = defaultdict(lambda: defaultdict(list)) # mergeSwitch -> busStop -> [(edges, stopObj), ....]
516
mergeSignals = {} # (switch, edges) -> signal
517
numFound = 0
518
for busStop, stops in stopRoutes.items():
519
for edgesBefore, stop in stops:
520
signal = None
521
signalEdgeIndex = 0
522
prevEdge = None
523
for i, edge in enumerate(edgesBefore):
524
node = net.getEdge(edge).getFromNode()
525
if node.getType() == "rail_signal":
526
tls = net.getTLS(node.getID())
527
for inLane, outLane, _ in tls.getConnections():
528
if (outLane.getEdge().getID() == edge
529
and (prevEdge is None or prevEdge == inLane.getEdge().getID())):
530
signal = tls.getID()
531
signalEdgeIndex = i
532
break
533
if edge in mergeSwitches:
534
numFound += 1
535
switchRoutes[edge][busStop].append((edgesBefore, stop))
536
if signal is None:
537
print("No signal found when approaching stop %s via switch %s along route %s" % (
538
busStop, edge, ','.join(edgesBefore)), file=sys.stderr)
539
# travel time from signal to stop
540
ttSignalStop = getTravelTime(net, edgesBefore[signalEdgeIndex:])
541
mergeSignals[(edge, edgesBefore)] = (signal, ttSignalStop)
542
prevEdge = edge
543
544
print("Found %s stops after merging switches and %s signals that guard switches" % (
545
numFound, len(set(mergeSignals.values()))))
546
return switchRoutes, mergeSignals
547
548
549
def getArrivalSecure(stop):
550
if stop.hasAttribute("arrival"):
551
return parseTime(stop.arrival)
552
elif stop.hasAttribute("until"):
553
return parseTime(stop.until) - parseTime(stop.getAttributeSecure("duration", "0"))
554
else:
555
return None
556
557
558
def computeSignalTimes(options, net, stopRoutes):
559
signalTimes = defaultdict(list) # signal -> [(timeAtSignal, stop), ...]
560
debugInfo = []
561
for _, stops in stopRoutes.items():
562
for edgesBefore, stop in stops:
563
if stop.hasAttribute("arrival"):
564
arrival = parseTime(stop.arrival)
565
elif stop.hasAttribute("until"):
566
arrival = parseTime(stop.until) - parseTime(stop.getAttributeSecure("duration", "0"))
567
else:
568
continue
569
for i, edge in enumerate(edgesBefore):
570
node = net.getEdge(edge).getFromNode()
571
if node.getType() == "rail_signal":
572
tls = net.getTLS(node.getID())
573
for _, outLane, __ in tls.getConnections():
574
if outLane.getEdge().getID() == edge:
575
signal = tls.getID()
576
ttSignalStop = getTravelTime(net, edgesBefore[i:])
577
timeAtSignal = arrival - ttSignalStop
578
signalTimes[signal].append((timeAtSignal, stop))
579
if signal == options.debugSignal:
580
debugInfo.append((timeAtSignal,
581
("%s vehID=%s prevTripId=%s passes signal %s to stop %s " +
582
"arrival=%s ttSignalStop=%s edges=%s") % (
583
humanReadableTime(timeAtSignal),
584
stop.vehID, stop.prevTripId,
585
signal, stop.busStop,
586
humanReadableTime(arrival), ttSignalStop,
587
edgesBefore)))
588
break
589
for signal in signalTimes.keys():
590
signalTimes[signal] = sorted(signalTimes[signal])
591
592
if options.debugSignal in signalTimes:
593
for _, info in sorted(debugInfo):
594
print(info)
595
busStops = set([s.busStop for a, s in signalTimes[options.debugSignal]])
596
arrivals = [a for a, s in signalTimes[options.debugSignal]]
597
print("Signal %s is passed %s times between %s and %s on approach to stops %s" % (
598
options.debugSignal, len(arrivals), arrivals[0], arrivals[-1], ' '.join(busStops)))
599
600
return signalTimes
601
602
603
def countPassingTrainsToOtherStops(options, signal, currentStop, begin, end, signalTimes):
604
count = 0
605
for timeAtSwitch, stop in signalTimes[signal]:
606
if timeAtSwitch < begin:
607
continue
608
if timeAtSwitch > end:
609
break
610
if stop.busStop == currentStop:
611
continue
612
count += 1
613
if options.debugSignal == signal:
614
print("vehicle %s (%s) passes signal %s at time %s (in between %s and %s)" % (
615
stop.prevTripId, stop.vehID, signal, timeAtSwitch, begin, end))
616
return count
617
618
619
def markOvertaken(options, vehicleStopRoutes, stopRoutes):
620
"""
621
mark stops that should not participate in constraint generation
622
once a vehicle appears to be "overtaken" (based on inconsistent
623
arrival/until timing or started/ended timing),
624
all subsequent stops of that vehicle should no longer be used for constraint generation.
625
"""
626
for vehicle, stopRoute in vehicleStopRoutes.items():
627
# Count number of stops after having been overtaken
628
overtaken = 0
629
# whether a warning was already given
630
ignored = False
631
632
for i, (edgesBefore, stop) in enumerate(stopRoute):
633
if not (stop.hasAttribute("arrival") and stop.hasAttribute("until")):
634
continue
635
636
started = parseTime(stop.started) if stop.hasAttribute("started") else None
637
ended = parseTime(stop.ended) if stop.hasAttribute("ended") else None
638
until = parseTime(stop.until)
639
parking = parseBool(stop.getAttributeSecure("parking", "false"))
640
if not overtaken:
641
arrival = parseTime(stop.arrival)
642
for edgesBefore2, stop2 in stopRoutes[stop.busStop]:
643
if stop2.vehID == stop.vehID:
644
continue
645
if not stop2.hasAttribute("arrival") or not stop2.hasAttribute("until"):
646
continue
647
parking2 = parseBool(stop2.getAttributeSecure("parking", "false"))
648
hasParking = parking or parking2
649
arrival2 = parseTime(stop2.arrival)
650
until2 = parseTime(stop2.until)
651
started2 = parseTime(stop2.started) if stop2.hasAttribute("started") else None
652
ended2 = parseTime(stop2.ended) if stop2.hasAttribute("ended") else None
653
654
swappedEnded = (ended2 is not None
655
# vehicle had not left but its schedule follower had
656
and (ended is None
657
# vehicle left after schedule follower
658
or ended2 < ended))
659
660
# if parking stops have the same until-time their depart order
661
# is undefined so we could get deadlocks
662
if options.skipParking and hasParking and until != until2 and not swappedEnded:
663
continue
664
if (arrival2 > arrival and (
665
# legacy: until replaced by ended
666
until2 < until or
667
(started2 is not None
668
# vehicle had not arrived but its schedule follower had
669
and (started is None
670
# vehicle arrived after schedule follower
671
or started2 < started)) or
672
swappedEnded)):
673
# ignore stops with inconsistency in the original schedule
674
# immediately but generate constraints for the stop
675
# where 'actual' overtaking was detected (and only ignore the subsequent stops)
676
overtaken = 2 if started is None else 1
677
ignored = started is None
678
ignoredInfo = " and ignored afterwards" if started is None else ""
679
print(("Vehicle %s %s overtaken by %s %s " +
680
"at stop %s (index %s)%s") %
681
(stop.vehID, formatStopTimes(arrival, until, started, ended),
682
stop2.vehID, formatStopTimes(arrival2, until2, started2, ended2),
683
stop.busStop, i, ignoredInfo),
684
file=sys.stderr)
685
break
686
elif hasParking and (until == until2 or (ended is not None and ended == ended2)):
687
overtaken = 1
688
ignored = True
689
if stop.vehID < stop2.vehID:
690
# only warn once
691
print(("Ambiguous departure order at stop %s" +
692
" (index %s) for %svehicle %s %s" +
693
" and %svehicle %s %s." +
694
" No constraints will be generated for them afterwards") % (
695
stop.busStop, i,
696
'parking ' if parking else ' ',
697
stop.vehID, formatStopTimes(arrival, until, started, ended),
698
'parking ' if parking2 else ' ',
699
stop2.vehID, formatStopTimes(arrival2, until2, started2, ended2),
700
),
701
file=sys.stderr)
702
break
703
704
if not overtaken:
705
if ended is not None and until - ended > options.prematureThreshold:
706
# train is running ahead of schedule and further schedule times are unreliable
707
overtaken = 1
708
ignored = True
709
print("Vehicle %s is running ahead of schedule by %ss at stop %s (index %s) and ignores further timings." % # noqa
710
(stop.vehID, int(until - ended), stop.busStop, i), file=sys.stderr)
711
712
# the stop where overtaking was detected can still be used for
713
# signal switching but subsequent stops cannot (as they might
714
# involve a mix of scheduled timing (arrival/until) and actual
715
# timings (started/ended)
716
if overtaken > 1:
717
# print("invalid veh=%s stop=%s arrival=%s until=%s" %
718
# (stop.vehID, stop.busStop,
719
# humanReadableTime(parseTime(stop.arrival)),
720
# humanReadableTime(parseTime(stop.until))))
721
if not ignored:
722
print("Vehicle %s was overtaken and starts to ignore schedule at stop %s (index %s)" %
723
(stop.vehID, stop.busStop, i),
724
file=sys.stderr)
725
ignored = True
726
if started is not None and ended is not None and options.abortUnorderedKeepActual:
727
print(" Stop %s (index %s) has 'started' and 'ended' and will be kept." %
728
(stop.busStop, i),
729
file=sys.stderr)
730
else:
731
stop.setAttribute("invalid", True)
732
733
if overtaken > 0:
734
overtaken += 1
735
736
737
def updateStartedEnded(options, net, stopEdges, stopRoutes, vehicleStopRoutes):
738
"""
739
replace arrival,until information with started,ended
740
At stops with parking=true, additional constraints are needed so that
741
vehicles without 'ended' delay their re-insertion until all vehicles with 'ended' have passed
742
"""
743
# signal -> [(tripID, otherSignal, otherTripID, limit, line, otherLine, vehID, otherVehID), ...]
744
limit = 1 + options.limit
745
conflicts = defaultdict(list)
746
# foe(parking)Insertion conflicts
747
fpiConflicts = defaultdict(list)
748
numConflicts = 0
749
numConflicts2 = 0
750
751
maxShift = 0
752
for busStop in sorted(stopRoutes.keys()):
753
stops = stopRoutes[busStop]
754
latestKnownTime = 0
755
shift = 0
756
stopEdge = stopEdges[busStop]
757
parkingEnded = []
758
759
for edgesBefore, stop in stops:
760
if stop.hasAttribute("ended") and stop.hasAttribute("until"):
761
ended = parseTime(stop.ended)
762
if ended > latestKnownTime:
763
latestKnownTime = ended
764
shift = max(shift, ended - parseTime(stop.until))
765
766
if parseBool(stop.getAttributeSecure("parking", "false")):
767
vehStops = vehicleStopRoutes[stop.vehID]
768
index = vehStops.index((edgesBefore, stop))
769
isPassing = index < len(vehStops) - 1
770
if isPassing:
771
# prevent overtaking by a vehicle without "ended" and earlier "until"
772
nextEdges = vehStops[index + 1][0]
773
parkingEnded.append((ended, stop, nextEdges))
774
775
elif stop.hasAttribute("started") and stop.hasAttribute("arrival"):
776
started = parseTime(stop.started)
777
if started > latestKnownTime:
778
latestKnownTime = started
779
shift = max(shift, started - parseTime(stop.arrival))
780
781
maxShift = max(maxShift, shift)
782
parkingEnded.sort(key=itemgetter(0))
783
784
for edgesBefore, stop in stops:
785
if stop.hasAttribute("started"):
786
stop.arrival = stop.started
787
elif stop.hasAttribute("arrival"):
788
stop.arrival = str(parseTime(stop.arrival) + shift)
789
if stop.hasAttribute("ended"):
790
stop.until = stop.ended
791
elif stop.hasAttribute("until"):
792
stop.until = str(parseTime(stop.until) + shift)
793
794
if len(parkingEnded) > 0:
795
ended, pStop, pNextEdges = parkingEnded[-1]
796
797
vehStops = vehicleStopRoutes[stop.vehID]
798
index = vehStops.index((edgesBefore, stop))
799
isPassing = index < len(vehStops) - 1
800
if isPassing:
801
802
pSignal = findSignal(net, (stopEdge,) + pNextEdges)
803
if parseBool(stop.getAttributeSecure("parking", "false")):
804
# we need an insertion constraint for insertion after parking
805
806
# find signal in nextEdges
807
nSignal = getDownstreamSignal(net, stopEdges, vehStops, index)
808
if pSignal is None or nSignal is None:
809
print(("Ignoring parking insertion conflict between %s and %s at stop '%s' " +
810
"because no rail signal was found after the stop") % (
811
stop.prevTripId, pStop.prevTripId, busStop), file=sys.stderr)
812
continue
813
814
# vehicles have already stopped so the new tripId applies
815
nTripID = stop.getAttributeSecure("tripId", stop.vehID)
816
pTripID = pStop.getAttributeSecure("tripId", pStop.vehID)
817
conflicts[nSignal].append(Conflict(nTripID, pSignal, pTripID, limit,
818
# attributes for adding comments
819
stop.line,
820
pStop.line,
821
stop.vehID,
822
pStop.vehID,
823
"foeEnded=%s " % humanReadableTime(ended),
824
None, # switch
825
stop.busStop,
826
"parking"))
827
numConflicts += 1
828
829
else:
830
# this is a foeInsertionConflicts and we need a normal constraint
831
832
# find signal in prior edges
833
nSignal = getUpstreamSignal(net, vehStops, index)
834
if pSignal is None:
835
print(("Ignoring foe parking insertion conflict between %s and %s at stop '%s' " +
836
"because no rail signal was found after the stop") % (
837
stop.prevTripId, pStop.prevTripId, busStop), file=sys.stderr)
838
continue
839
if nSignal is None:
840
print(("Ignoring foe parking insertion conflict between %s and %s at stop '%s' " +
841
"because no rail signal was found before the stop") % (
842
stop.prevTripId, pStop.prevTripId, busStop), file=sys.stderr)
843
continue
844
845
# vehicles have already stopped so the new tripId applies
846
nStopBefore = vehStops[index - 1][1]
847
nTripID = nStopBefore.getAttributeSecure("tripId", nStopBefore.vehID)
848
pTripID = pStop.getAttributeSecure("tripId", pStop.vehID)
849
# marked as inactive because they are causing more
850
# deadlocks than they solve but might already be usefully (i.e. via TraCI)
851
fpiConflicts[nSignal].append(Conflict(nTripID, pSignal, pTripID, limit,
852
# attributes for adding comments
853
nStopBefore.line,
854
pStop.line,
855
nStopBefore.vehID,
856
pStop.vehID,
857
"foeEnded=%s " % humanReadableTime(ended),
858
None, # switch
859
stop.busStop,
860
"foeParking",
861
active=False))
862
numConflicts2 += 1
863
864
if busStop == options.debugStop and shift > 0:
865
print("Shifted stop times at %s by %s" % (busStop, shift))
866
867
if options.verbose and maxShift > 0:
868
print("Shifted stop times by up to %s" % maxShift)
869
870
if options.verbose and numConflicts > 0:
871
print("Found %s parking insertion conflicts" % numConflicts)
872
if options.verbose and numConflicts2 > 0:
873
print("Found %s foe parking insertion conflicts" % numConflicts2)
874
875
return conflicts, fpiConflicts
876
877
878
def addCommonStop(options, switch, edgesBefore, stop, edgesBefore2, stop2, vehicleStopRoutes, stopRoutes2):
879
""" add more items to stopRoutes2 for the common
880
busStop but keep the edgeBefore of the original stops
881
"""
882
assert stop.busStop != stop2.busStop
883
if stop.vehID == stop2.vehID:
884
return
885
route = vehicleStopRoutes[stop.vehID]
886
route2 = vehicleStopRoutes[stop2.vehID]
887
stopIndex = route.index((edgesBefore, stop))
888
stopIndex2 = route2.index((edgesBefore2, stop2))
889
routeIndex = edgesBefore.index(switch)
890
routeIndex2 = edgesBefore2.index(switch)
891
# advance both routes until the routes diverge, one route ends or a common stop is found
892
while stopIndex < len(route) and stopIndex2 < len(route2):
893
eb, s = route[stopIndex]
894
eb2, s2 = route2[stopIndex2]
895
e = eb[routeIndex]
896
e2 = eb2[routeIndex2]
897
# print(stopIndex, routeIndex, stopIndex2, routeIndex2, len(eb), len(eb2))
898
if e != e2:
899
# routes diverge
900
# print("e=%s e2=%s" % (e, e2))
901
return
902
if (routeIndex + 1 == len(eb) and
903
routeIndex2 + 1 == len(eb2) and
904
s.busStop == s2.busStop):
905
# found common stop
906
# print("switch=%s veh=%s veh2=%s commonStop=%s" % (switch,
907
# stop.vehID, stop2.vehID, s.busStop))
908
if (edgesBefore2, s2) not in stopRoutes2[s.busStop]:
909
s2copy = copy.copy(s2)
910
s2copy.prevTripId = stop2.prevTripId
911
s2copy.setAttribute("intermediateStop", copy.copy(stop2))
912
s2copy.setAttribute("edgesBeforeCommon", eb2)
913
s2copy.setAttribute("otherVeh", stop.vehID)
914
stopRoutes2[s.busStop].append((edgesBefore2, s2copy))
915
if s.busStop != stop.busStop and ((edgesBefore, s) not in stopRoutes2[s.busStop]):
916
scopy = copy.copy(s)
917
scopy.prevTripId = stop.prevTripId
918
scopy.setAttribute("intermediateStop", copy.copy(stop))
919
scopy.setAttribute("edgesBeforeCommon", eb)
920
scopy.setAttribute("otherVeh", stop2.vehID)
921
stopRoutes2[s.busStop].append((edgesBefore, scopy))
922
return
923
# advance along routes
924
if routeIndex + 1 == len(eb):
925
routeIndex = 0
926
stopIndex += 1
927
# skip duplicate stops since they do not add edges
928
while stopIndex < len(route) and len(route[stopIndex][0]) == 0:
929
stopIndex += 1
930
else:
931
routeIndex += 1
932
933
if routeIndex2 + 1 == len(eb2):
934
routeIndex2 = 0
935
stopIndex2 += 1
936
while stopIndex2 < len(route2) and len(route2[stopIndex2][0]) == 0:
937
stopIndex2 += 1
938
else:
939
routeIndex2 += 1
940
941
if stop.vehID == options.debugVehicle:
942
print(("No common stop found after switch %s for vehicle %s with stop %s (%s, %s)"
943
+ " and intermediate stop of vehicle %s at %s (%s, %s)") % (
944
switch, stop.vehID, stop.busStop,
945
humanReadableTime(parseTime(stop.arrival)),
946
humanReadableTime(parseTime(stop.until)),
947
stop2.vehID, stop2.busStop,
948
humanReadableTime(parseTime(stop2.arrival)),
949
humanReadableTime(parseTime(stop2.until)),
950
))
951
952
953
def findConflicts(options, net, switchRoutes, mergeSignals, signalTimes, stopEdges, vehicleStopRoutes):
954
"""find stops that target the same busStop from different branches of the
955
prior merge switch and establish their ordering"""
956
957
numConflicts = 0
958
numRedundant = 0
959
numIgnoredConflicts = 0
960
numIgnoredStops = 0
961
# signal -> [(tripID, otherSignal, otherTripID, limit, line, otherLine, vehID, otherVehID), ...]
962
conflicts = defaultdict(list)
963
intermediateParkingConflicts = defaultdict(list)
964
965
for switch in sorted(switchRoutes.keys()):
966
stopRoutes2 = switchRoutes[switch]
967
numSwitchConflicts = 0
968
numRedundantSwitchConflicts = 0
969
numIgnoredSwitchConflicts = 0
970
numIgnoredSwitchStops = 0
971
if switch == options.debugSwitch:
972
print("Switch %s lies ahead of busStops %s" % (switch, stopRoutes2.keys()))
973
974
# detect approaches that skip stops (#8943) and add extra items
975
stopRoutes3 = defaultdict(list)
976
stopsAfterSwitch = defaultdict(set) # edge -> stops
977
for busStop, stops in stopRoutes2.items():
978
stopsAfterSwitch[stopEdges[busStop]].add(busStop)
979
for busStop, stops in stopRoutes2.items():
980
for edgesBefore, stop in stops:
981
intermediateStops = set()
982
for edge in edgesBefore:
983
for s in stopsAfterSwitch[edge]:
984
if s != busStop:
985
intermediateStops.add(s)
986
if len(intermediateStops) != 0:
987
# try to establish an order between vehicles from different arms
988
# of the switch even if their immediate stops differs
989
# this is possible (and necessary) if they have a common stop
990
# and their routes are identical between the merging switch and
991
# the first common stop.
992
# note: the first common stop may differ from all stops in stopRoutes2
993
for busStop2, stops2 in stopRoutes2.items():
994
if busStop2 in intermediateStops:
995
for edgesBefore2, stop2 in stops2:
996
addCommonStop(options, switch, edgesBefore, stop,
997
edgesBefore2, stop2, vehicleStopRoutes, stopRoutes3)
998
# print("Stop after switch %s at %s by %s (%s, %s) passes intermediate stops %s" % (
999
# switch, busStop, stop.vehID,
1000
# humanReadableTime(parseTime(stop.arrival)),
1001
# humanReadableTime(parseTime(stop.until)),
1002
# ",".join(intermediateStops)
1003
# ))
1004
1005
# delay dict merge until all extra stops have been found
1006
for busStop, stops in stopRoutes2.items():
1007
stopRoutes3[busStop] += stops
1008
1009
for busStop, stops in stopRoutes3.items():
1010
arrivals = []
1011
for edges, stop in stops:
1012
if stop.hasAttribute("arrival"):
1013
arrival = parseTime(stop.arrival)
1014
elif stop.hasAttribute("until"):
1015
arrival = parseTime(stop.until) - parseTime(stop.getAttributeSecure("duration", "0"))
1016
else:
1017
print("ignoring stop at %s without schedule information (arrival, until)" % busStop)
1018
continue
1019
if stop.getAttributeSecure("invalid", False):
1020
numIgnoredSwitchStops += 1
1021
numIgnoredStops += 1
1022
if not options.writeInactive:
1023
continue
1024
arrivals.append((arrival, edges, stop))
1025
arrivals.sort(key=itemgetter(0))
1026
arrivalsBySignal = defaultdict(list)
1027
for (pArrival, pEdges, pStop), (nArrival, nEdges, nStop) in zip(arrivals[:-1], arrivals[1:]):
1028
pSignal, pTimeSiSt = mergeSignals[(switch, pEdges)]
1029
nSignal, nTimeSiSt = mergeSignals[(switch, nEdges)]
1030
if switch == options.debugSwitch:
1031
print(pSignal, nSignal, pStop, nStop)
1032
if (pSignal != nSignal and pSignal is not None and nSignal is not None
1033
and pStop.vehID != nStop.vehID):
1034
if options.skipParking and parseBool(nStop.getAttributeSecure("parking", "false")):
1035
print("ignoring stop at %s for parking vehicle %s (%s, %s)" % (
1036
busStop, nStop.vehID, humanReadableTime(nArrival),
1037
(humanReadableTime(parseTime(nStop.until)) if nStop.hasAttribute("until") else "-")))
1038
numIgnoredConflicts += 1
1039
numIgnoredSwitchConflicts += 1
1040
continue
1041
if options.skipParking and parseBool(pStop.getAttributeSecure("parking", "false")):
1042
print("ignoring stop at %s for %s (%s, %s) after parking vehicle %s (%s, %s)" % (
1043
busStop, nStop.vehID, humanReadableTime(nArrival),
1044
(humanReadableTime(parseTime(nStop.until)) if nStop.hasAttribute("until") else "-"),
1045
pStop.vehID, humanReadableTime(pArrival),
1046
(humanReadableTime(parseTime(pStop.until)) if pStop.hasAttribute("until") else "-")))
1047
numIgnoredConflicts += 1
1048
numIgnoredSwitchConflicts += 1
1049
continue
1050
if (nStop.intermediateStop and pStop.intermediateStop
1051
and nStop.intermediateStop.busStop == pStop.intermediateStop.busStop):
1052
# intermediate conflict was added via other foes and this particular conflict is a normal one
1053
continue
1054
numConflicts += 1
1055
numSwitchConflicts += 1
1056
# check for trains that pass the switch in between the
1057
# current two trains (heading to another stop) and raise the limit
1058
limit = 1
1059
pTimeAtSignal = pArrival - pTimeSiSt
1060
nTimeAtSignal = nArrival - nTimeSiSt
1061
end = nTimeAtSignal + options.delay
1062
if options.verbose and options.debugSignal == pSignal:
1063
print("check vehicles between %s and %s (including delay %s) at signal %s pStop=%s nStop=%s" % (
1064
humanReadableTime(pTimeAtSignal),
1065
humanReadableTime(end), options.delay, pSignal,
1066
pStop, nStop))
1067
times = "arrival=%s foeArrival=%s " % (humanReadableTime(nArrival), humanReadableTime(pArrival))
1068
info = getIntermediateInfo(pStop, nStop)
1069
isIntermediateParking = nStop.intermediateStop and parseBool(
1070
nStop.intermediateStop.getAttributeSecure("parking", "false"))
1071
active = not nStop.getAttributeSecure(
1072
"invalid", False) and not pStop.getAttributeSecure("invalid", False)
1073
if isIntermediateParking:
1074
# intermediateParkingConflicts: train order isn't determined at the switch
1075
# but rather when the second vehicle leaves its parking stop
1076
stopEdge = stopEdges[nStop.intermediateStop.busStop]
1077
signal = findSignal(net, (stopEdge,) + nStop.edgesBeforeCommon)
1078
if signal is None:
1079
print(("Ignoring intermediate parking insertion conflict between %s and %s at stop '%s' " +
1080
"because no rail signal was found after the stop") %
1081
(nStop.tripID, pStop.prevTripId, nStop.intermediateStop.busStop), file=sys.stderr)
1082
continue
1083
times = "intermediateArrival=%s %s" % (humanReadableTime(
1084
parseTime(nStop.intermediateStop.arrival)), times)
1085
info = "intermediateParking " + info
1086
intermediateParkingConflicts[signal].append(Conflict(nStop.prevTripId, signal,
1087
pStop.prevTripId, limit,
1088
# attributes for adding comments
1089
nStop.prevLine, pStop.prevLine,
1090
nStop.vehID, pStop.vehID,
1091
times, switch,
1092
nStop.intermediateStop.busStop,
1093
info, active))
1094
else:
1095
info = getIntermediateInfo(pStop, nStop)
1096
limit += countPassingTrainsToOtherStops(options, pSignal,
1097
busStop, pTimeAtSignal, end, signalTimes)
1098
conflicts[nSignal].append(Conflict(nStop.prevTripId, pSignal, pStop.prevTripId, limit,
1099
# attributes for adding comments
1100
nStop.prevLine, pStop.prevLine,
1101
nStop.vehID, pStop.vehID,
1102
times, switch,
1103
nStop.busStop,
1104
info, active))
1105
if options.redundant >= 0:
1106
prevBegin = pTimeAtSignal
1107
for p2Arrival, p2Stop in reversed(arrivalsBySignal[pSignal]):
1108
if pArrival - p2Arrival > options.redundant:
1109
break
1110
if (nStop.intermediateStop and pStop.intermediateStop
1111
and nStop.intermediateStop.busStop == pStop.intermediateStop.busStop):
1112
# intermediate conflict was added via other foes
1113
# and this particular conflict is a normal one
1114
continue
1115
if isIntermediateParking:
1116
# no redundant parking insertion conflicts for intermediate stops
1117
continue
1118
numRedundant += 1
1119
numRedundantSwitchConflicts += 1
1120
p2TimeAtSignal = p2Arrival - pTimeSiSt
1121
limit += 1
1122
limit += countPassingTrainsToOtherStops(options, pSignal,
1123
busStop, p2TimeAtSignal, prevBegin, signalTimes)
1124
info = getIntermediateInfo(p2Stop, nStop)
1125
times = "arrival=%s foeArrival=%s " % (
1126
humanReadableTime(nArrival), humanReadableTime(p2Arrival))
1127
active = not nStop.getAttributeSecure(
1128
"invalid", False) and not pStop.getAttributeSecure("invalid", False)
1129
conflicts[nSignal].append(Conflict(nStop.prevTripId, pSignal, p2Stop.prevTripId, limit,
1130
# attributes for adding comments
1131
nStop.prevLine, p2Stop.prevLine,
1132
nStop.vehID,
1133
p2Stop.vehID,
1134
times, switch,
1135
nStop.busStop,
1136
info, active))
1137
prevBegin = p2TimeAtSignal
1138
1139
if pSignal is not None and not (
1140
options.skipParking and
1141
parseBool(pStop.getAttributeSecure("parking", "false"))):
1142
arrivalsBySignal[pSignal].append((pArrival, pStop))
1143
1144
if options.verbose:
1145
print("Found %s conflicts at switch %s" % (numSwitchConflicts, switch))
1146
if numRedundantSwitchConflicts > 0:
1147
print("Found %s redundant conflicts at switch %s" % (numRedundantSwitchConflicts, switch))
1148
1149
if numIgnoredSwitchConflicts > 0 or numIgnoredSwitchStops > 0:
1150
print("Ignored %s conflicts and % stops at switch %s" %
1151
(numIgnoredSwitchConflicts, numIgnoredSwitchStops, switch))
1152
1153
print("Found %s conflicts" % numConflicts)
1154
if numRedundant > 0:
1155
print("Found %s redundant conflicts" % numRedundant)
1156
1157
if numIgnoredConflicts > 0 or numIgnoredStops > 0:
1158
print("Ignored %s conflicts and %s stops" % (numIgnoredConflicts, numIgnoredStops))
1159
return conflicts, intermediateParkingConflicts
1160
1161
1162
def getIntermediateInfo(pStop, nStop):
1163
info = []
1164
if pStop.intermediateStop:
1165
info.append("foeIntermediateStop=%s" % pStop.intermediateStop.busStop)
1166
if nStop.intermediateStop:
1167
info.append("intermediateStop=%s" % nStop.intermediateStop.busStop)
1168
# if pStop.otherVeh:
1169
# info.append("otherVeh=%s" % pStop.otherVeh)
1170
# if nStop.otherVeh:
1171
# info.append("foeOtherVeh=%s" % nStop.otherVeh)
1172
return ' '.join(info)
1173
1174
1175
def findSignal(net, nextEdges, reverse=False):
1176
prevEdge = None
1177
for i, edge in enumerate(nextEdges):
1178
if reverse:
1179
edge, prevEdge = prevEdge, edge
1180
if edge is None:
1181
continue
1182
1183
node = net.getEdge(edge).getFromNode()
1184
if node.getType() == "rail_signal":
1185
tls = net.getTLS(node.getID())
1186
for inLane, outLane, _ in tls.getConnections():
1187
if (outLane.getEdge().getID() == edge
1188
and prevEdge == inLane.getEdge().getID()):
1189
return tls.getID()
1190
if not reverse:
1191
prevEdge = edge
1192
return None
1193
1194
1195
def getDownstreamSignal(net, stopEdges, vehStops, stopIndex):
1196
nextEdges = vehStops[stopIndex + 1][0]
1197
stop = vehStops[stopIndex][1]
1198
return findSignal(net, (stopEdges[stop.busStop],) + nextEdges)
1199
1200
1201
def getUpstreamSignal(net, vehStops, stopIndex):
1202
prevEdges = vehStops[stopIndex][0]
1203
if stopIndex > 0:
1204
# prepend on more edge from further back
1205
prevEdges = vehStops[stopIndex - 1][0][-1:] + prevEdges
1206
return findSignal(net, list(reversed(prevEdges)), True)
1207
1208
1209
def findInsertionConflicts(options, net, stopEdges, stopRoutes, vehicleStopRoutes, departTimes, writeInactive=False):
1210
"""find routes that start at a stop with a traffic light at end of the edge
1211
and routes that pass this stop. Ensure insertion happens in the correct order
1212
(finds constraints on insertion)
1213
"""
1214
# signal -> [(tripID, otherSignal, otherTripID, limit, line, otherLine, vehID, otherVehID), ...]
1215
conflicts = defaultdict(list)
1216
numConflicts = 0
1217
numIgnoredConflicts = 0
1218
for busStop, stops in stopRoutes.items():
1219
if busStop == options.debugStop:
1220
print("findInsertionConflicts at stop %s%s" % (
1221
busStop, " (writeInactive)" if writeInactive else ""))
1222
untils = []
1223
for edgesBefore, stop in stops:
1224
if stop.hasAttribute("until") and not options.untilFromDuration:
1225
until = parseTime(stop.until)
1226
elif stop.hasAttribute("arrival"):
1227
until = parseTime(stop.arrival) + parseTime(stop.getAttributeSecure("duration", "0"))
1228
else:
1229
continue
1230
untils.append((until, edgesBefore, stop))
1231
# only use 'until' for sorting and keep the result stable otherwise
1232
untils.sort(key=itemgetter(0))
1233
prevPassing = None
1234
for i, (nUntil, nEdges, nStop) in enumerate(untils):
1235
nIsBidiStop = nStop.busStop != busStop
1236
nVehStops = vehicleStopRoutes[nStop.vehID]
1237
nIndex = nVehStops.index((nEdges, nStop))
1238
nIsPassing = nIndex < len(nVehStops) - 1
1239
nIsDepart = len(nEdges) == 1 and nIndex == 0
1240
nInvalid = nStop.getAttributeSecure("invalid", False)
1241
if options.verbose and busStop == options.debugStop:
1242
print("%s n: %s %s %s %s %s passing: %s depart: %s%s%s" %
1243
(i, humanReadableTime(nUntil), nStop.tripId, nStop.vehID, nIndex, len(nVehStops),
1244
nIsPassing, nIsDepart,
1245
(" bidiStop: %s" % nStop.busStop) if nIsBidiStop else "",
1246
" invalid" if nInvalid else ""))
1247
# ignore duplicate bidiStop vs bidiStop conflicts
1248
if prevPassing is not None and nIsDepart and not nIsBidiStop:
1249
pUntil, pEdges, pStop = prevPassing
1250
pVehStops = vehicleStopRoutes[pStop.vehID]
1251
pIndex = pVehStops.index((pEdges, pStop))
1252
pIsBidiStop = pStop.busStop != busStop
1253
pIsPassing = pIndex < len(pVehStops) - 1
1254
pIsDepart = len(pEdges) == 1 and pIndex == 0
1255
# usually, subsequent departures do not require constraints
1256
# (unless depart, until and ended out of sync)
1257
if not pIsDepart or (departTimes[nStop.vehID] is not None
1258
and departTimes[pStop.vehID] is not None
1259
and departTimes[nStop.vehID] < departTimes[pStop.vehID]):
1260
# find edges after stop
1261
if busStop == options.debugStop:
1262
print(i,
1263
"p:", humanReadableTime(pUntil), pStop.tripId, pStop.vehID, pIndex, len(pVehStops),
1264
"n:", humanReadableTime(nUntil), nStop.tripId, nStop.vehID, nIndex, len(nVehStops))
1265
if nIsPassing:
1266
# usually both vehicles move past the stop
1267
if pIsBidiStop and not pIsPassing:
1268
pSignal = getUpstreamSignal(net, pVehStops, pIndex)
1269
else:
1270
pSignal = getDownstreamSignal(net, stopEdges, pVehStops, pIndex)
1271
nSignal = getDownstreamSignal(net, stopEdges, nVehStops, nIndex)
1272
if pSignal is None or nSignal is None:
1273
print(("Ignoring insertion conflict between %s and %s at stop '%s' " +
1274
"because no rail signal was found after the stop") % (
1275
nStop.prevTripId, pStop.prevTripId, busStop), file=sys.stderr)
1276
continue
1277
# check for inconsistent ordering
1278
active = True
1279
isInvalid = pStop.getAttributeSecure("invalid", False)
1280
if isInvalid:
1281
active = False
1282
numIgnoredConflicts += 1
1283
if not writeInactive:
1284
continue
1285
elif writeInactive:
1286
continue
1287
# predecessor tripId after stop is needed
1288
limit = 1 # recheck
1289
pTripId = pStop.getAttributeSecure("tripId", pStop.vehID)
1290
times = "until=%s foeUntil=%s " % (humanReadableTime(nUntil), humanReadableTime(pUntil))
1291
info = "" if nStop.busStop == pStop.busStop else "foeStop=%s" % pStop.busStop
1292
tag = "insertionOrder" if pIsDepart else None
1293
conflicts[nSignal].append(Conflict(nStop.prevTripId, pSignal, pTripId, limit,
1294
# attributes for adding comments
1295
nStop.prevLine,
1296
pStop.prevLine,
1297
nStop.vehID,
1298
pStop.vehID,
1299
times,
1300
switch=None,
1301
busStop=nStop.busStop,
1302
info=info,
1303
active=active,
1304
tag=tag))
1305
numConflicts += 1
1306
if busStop == options.debugStop:
1307
print(" found insertionConflict pSignal=%s nSignal=%s pTripId=%s" % (
1308
pSignal, nSignal, pTripId)),
1309
1310
if (nIsPassing or nIsBidiStop) and (not nInvalid or writeInactive):
1311
prevPassing = (nUntil, nEdges, nStop)
1312
1313
if writeInactive:
1314
if numConflicts > 0:
1315
print("Found %s inactive insertion conflicts" % numConflicts)
1316
else:
1317
print("Found %s insertion conflicts" % numConflicts)
1318
if numIgnoredConflicts > 0:
1319
print("Ignored %s insertion conflicts" % (numIgnoredConflicts))
1320
return conflicts
1321
1322
1323
def findFoeInsertionConflicts(options, net, stopEdges, stopRoutes, vehicleStopRoutes):
1324
"""find routes that start at a stop with a traffic light at end of the edge
1325
and routes that pass this stop. Ensure insertion happens in the correct order
1326
(finds constrains on entering the stop segment ahead of insertion)
1327
"""
1328
# signal -> [(tripID, otherSignal, otherTripID, limit, line, otherLine, vehID, otherVehID), ...]
1329
conflicts = defaultdict(list)
1330
numConflicts = 0
1331
numIgnoredConflicts = 0
1332
for busStop, stops in stopRoutes.items():
1333
if busStop == options.debugStop:
1334
print("findFoeInsertionConflicts at stop %s" % busStop)
1335
untils = []
1336
for edgesBefore, stop in stops:
1337
if stop.hasAttribute("until") and not options.untilFromDuration:
1338
until = parseTime(stop.until)
1339
elif stop.hasAttribute("arrival"):
1340
until = parseTime(stop.arrival) + parseTime(stop.getAttributeSecure("duration", "0"))
1341
else:
1342
continue
1343
if stop.getAttributeSecure("invalid", False) and not options.writeInactive:
1344
continue
1345
untils.append((until, edgesBefore, stop))
1346
# only use 'until' for sorting and keep the result stable otherwise
1347
untils.sort(key=itemgetter(0))
1348
prevDepart = None
1349
for i, (nUntil, nEdges, nStop) in enumerate(untils):
1350
nIsBidiStop = nStop.busStop != busStop
1351
nVehStops = vehicleStopRoutes[nStop.vehID]
1352
nVehStops = vehicleStopRoutes[nStop.vehID]
1353
nIndex = nVehStops.index((nEdges, nStop))
1354
nIsPassing = nIndex < len(nVehStops) - 1
1355
nIsDepart = len(nEdges) == 1 and nIndex == 0
1356
if options.verbose and busStop == options.debugStop:
1357
print("%s n: %s %s %s %s %s passing: %s depart: %s%s" %
1358
(i, humanReadableTime(nUntil), nStop.tripId, nStop.vehID, nIndex, len(nVehStops),
1359
nIsPassing, nIsDepart, (" bidiStop: %s" % nStop.busStop) if nIsBidiStop else ""))
1360
# ignore duplicate bidiStop vs bidiStop conflicts
1361
if prevDepart is not None and nIsPassing and not nIsDepart and not nIsBidiStop:
1362
pUntil, pEdges, pStop = prevDepart
1363
pVehStops = vehicleStopRoutes[pStop.vehID]
1364
pIndex = pVehStops.index((pEdges, pStop))
1365
# no need to constrain subsequent passing (simulation should maintain ordering)
1366
if len(nEdges) > 1 or nIndex > 0:
1367
# find edges after stop
1368
if busStop == options.debugStop:
1369
print(i,
1370
"p:", humanReadableTime(pUntil), pStop.tripId, pStop.vehID, pIndex, len(pVehStops),
1371
"n:", humanReadableTime(nUntil), nStop.tripId, nStop.vehID, nIndex, len(nVehStops))
1372
# insertion vehicle must pass signal after the stop
1373
pSignal = getDownstreamSignal(net, stopEdges, pVehStops, pIndex)
1374
if pSignal is None:
1375
print(("Ignoring insertion foe conflict between %s and %s at stop '%s' " +
1376
"because no rail signal was found after the stop") % (
1377
nStop.prevTripId, pStop.prevTripId, busStop), file=sys.stderr)
1378
continue
1379
# passing vehicle must wait before the stop
1380
nSignal = getUpstreamSignal(net, nVehStops, nIndex)
1381
if nSignal is None:
1382
print(("Ignoring foe insertion conflict between %s and %s at stop '%s' " +
1383
"because no rail signal was found before the stop") % (
1384
nStop.prevTripId, pStop.prevTripId, busStop), file=sys.stderr)
1385
continue
1386
1387
# check for inconsistent ordering
1388
if pStop.getAttributeSecure("invalid", False):
1389
numIgnoredConflicts += 1
1390
if not options.writeInactive:
1391
continue
1392
1393
if options.skipParking:
1394
if parseBool(nStop.getAttributeSecure("parking", "false")):
1395
print("ignoring stop at %s for parking vehicle %s (%s, %s)" % (
1396
busStop, nStop.vehID, humanReadableTime(nUntil),
1397
(humanReadableTime(parseTime(nStop.until)) if nStop.hasAttribute("until") else "-")))
1398
numIgnoredConflicts += 1
1399
continue
1400
# if parseBool(pStop.getAttributeSecure("parking", "false")):
1401
# # additional check for until times
1402
# print("ignoring stop at %s for %s (%s, %s) after parking vehicle %s (%s, %s)" % (
1403
# busStop, nStop.vehID, humanReadableTime(nUntil),
1404
# (humanReadableTime(parseTime(nStop.until)) if nStop.hasAttribute("until") else "-"),
1405
# pStop.vehID, humanReadableTime(pUntil),
1406
# (humanReadableTime(parseTime(pStop.until)) if pStop.hasAttribute("until") else "-")))
1407
# numIgnoredConflicts += 1
1408
# continue
1409
1410
# hotfix for strange input
1411
pNextStop = pVehStops[pIndex + 1][1]
1412
if pNextStop.lane is not None:
1413
print(("Ignoring foe insertion conflict between %s and %s at stop '%s' " +
1414
"because the inserted train does not leave the stop edge (laneStop)") % (
1415
nStop.prevTripId, pStop.prevTripId, busStop), file=sys.stderr)
1416
continue
1417
1418
# predecessor tripId after stop is needed
1419
limit = 1 # recheck
1420
pTripId = pStop.getAttributeSecure("tripId", pStop.vehID)
1421
times = "arrival=%s foeArrival=%s " % (humanReadableTime(nUntil), humanReadableTime(pUntil))
1422
info = ""
1423
if nStop.busStop != pStop.busStop:
1424
info += "foeStop=%s" % pStop.busStop
1425
active = not nStop.getAttributeSecure(
1426
"invalid", False) and not pStop.getAttributeSecure("invalid", False)
1427
conflicts[nSignal].append(Conflict(nStop.prevTripId, pSignal, pTripId, limit,
1428
# attributes for adding comments
1429
nStop.prevLine,
1430
pStop.prevLine,
1431
nStop.vehID, pStop.vehID,
1432
times,
1433
switch=None,
1434
busStop=nStop.busStop,
1435
info=info, active=active))
1436
numConflicts += 1
1437
if busStop == options.debugStop:
1438
print(" found foe insertion conflict pSignal=%s nSignal=%s pVehId=%s pTripId=%s" % (
1439
pSignal, nSignal, pStop.vehID, pTripId)),
1440
1441
if nIsDepart and nIsPassing:
1442
prevDepart = (nUntil, nEdges, nStop)
1443
1444
if numConflicts > 0:
1445
print("Found %s foe insertion conflicts" % numConflicts)
1446
if numIgnoredConflicts > 0:
1447
print("Ignored %s foe insertion conflicts" % (numIgnoredConflicts))
1448
return conflicts
1449
1450
1451
def getBidiID(net, edge):
1452
bidi = net.getEdge(edge).getBidi()
1453
if bidi:
1454
return bidi.getID()
1455
else:
1456
return None
1457
1458
1459
def findBidiConflicts(options, net, stopEdges, uniqueRoutes, stopRoutes, vehicleStopRoutes):
1460
"""find routes that pass a bidirectional edge sequence between stops in opposite direction
1461
"""
1462
# signal -> [(tripID, otherSignal, otherTripID, limit, line, otherLine, vehID, otherVehID), ...]
1463
conflicts = defaultdict(list)
1464
1465
if not options.bidiConflicts:
1466
return conflicts
1467
1468
numConflicts = 0
1469
numIgnoredConflicts = 0
1470
1471
# bidiSections = findBidiSections(options, uniqueRoutes, net)
1472
# bidiRoutes = find
1473
edge2Route = defaultdict(list) # edge -> [route, route2, ...]
1474
for route in uniqueRoutes:
1475
for edge in route:
1476
edge2Route[edge].append(route)
1477
usedBidi = set()
1478
for edge in edge2Route:
1479
if getBidiID(net, edge) in edge2Route:
1480
usedBidi.add(edge)
1481
1482
for busStop in sorted(stopRoutes.keys()):
1483
stops = stopRoutes[busStop]
1484
# group all approaches via the same edges
1485
approaches = defaultdict(list) # edgesBefore -> [stop, stop2, ...]
1486
for edgesBefore, stop in stops:
1487
if not edgesBefore:
1488
continue
1489
approaches[edgesBefore].append(stop)
1490
1491
for edgesBefore in sorted(approaches.keys()):
1492
stops = approaches[edgesBefore]
1493
oRoutes = set()
1494
stopEdgeBidi = getBidiID(net, edgesBefore[-1])
1495
for edge in edgesBefore:
1496
if edge in usedBidi:
1497
bidi = getBidiID(net, edge)
1498
for route in edge2Route[bidi]:
1499
# insertion on the stopEdgeBidi may also define a bidi
1500
# conflict (i.e. with a foe vehicle that has delayed insertion w.r.t its schedule)
1501
if stopEdgeBidi not in route[1:]:
1502
oRoutes.add(route)
1503
if oRoutes:
1504
# print(edgesBefore, stop, oRoutes)
1505
1506
# different vehicles in approaches could have a different routes
1507
# so we have to find the point of divergence separately
1508
bidiBefore = filter(lambda x: x is not None, [getBidiID(net, e) for e in edgesBefore])
1509
bidiBefore = list(reversed(list(bidiBefore)))
1510
for stop in stops:
1511
vehID = stop.vehID
1512
stopRoute = vehicleStopRoutes[vehID]
1513
stopIndex = stopRoute.index((edgesBefore, stop))
1514
arrivals = defaultdict(list) # (bidiStart, bidiEnd) -> (pArrival, pStop, sIb, sI2)
1515
for route in sorted(oRoutes):
1516
for vehID2 in uniqueRoutes[route]:
1517
if vehID2 == vehID:
1518
continue
1519
stopRoute2 = vehicleStopRoutes[vehID2]
1520
# find first stop where stopRoute2 diverges from stopRoute
1521
# since the route could be looped we must find all points of divergence
1522
sI2b = -1 # stop index of previous divergence
1523
prevEdge = None
1524
needInitialDivergence = False
1525
for sI2, (edgesBefore2, stop2) in enumerate(stopRoute2):
1526
if sI2 == 0:
1527
# this is an insertion related conflict and we don't want to find a signal
1528
# before the insertion edge
1529
continue
1530
if sI2 < sI2b:
1531
continue
1532
for e in edgesBefore2:
1533
if e in bidiBefore:
1534
if needInitialDivergence:
1535
continue
1536
# if the opposite train enters the conflict section with a reversal
1537
# this doesn't provide for a useful entry signal
1538
# (we still call findDivergence to advance sI2b)
1539
ignoreConflict = prevEdge == getBidiID(net, e)
1540
1541
# found the start of the conflict zone, now we need to find the end
1542
oppositeSection = [] # only for debug output
1543
sI2b = findDivergence(net, arrivals,
1544
getEdges(stopRoute, stopIndex, getBidiID(net, e), False),
1545
getEdges(stopRoute2, sI2, e, True),
1546
stopRoute2, sI2, e,
1547
ignoreConflict,
1548
oppositeSection)
1549
1550
if (vehID == options.debugVehicle
1551
and vehID2 == options.debugFoeVehicle
1552
and (stop.busStop == options.debugStop
1553
or options.debugStop is None)):
1554
print("Opposite section when approaching stop %s oppositeStopIndex %s " % (
1555
stop.busStop, sI2b))
1556
for e in oppositeSection:
1557
print("edge:%s" % e)
1558
1559
break
1560
else:
1561
needInitialDivergence = False
1562
prevEdge = e
1563
if sI2b is None:
1564
# no divergence found
1565
break
1566
# after finding a conflict, we first need to find a new divergence
1567
# as starting point for the next bidi conflict
1568
needInitialDivergence = True
1569
1570
for conflict in collectBidiConflicts(options, net, vehicleStopRoutes, stop,
1571
stopRoute, edgesBefore, arrivals):
1572
if conflict is None:
1573
numIgnoredConflicts += 1
1574
else:
1575
conflicts[conflict.signal].append(conflict)
1576
numConflicts += 1
1577
1578
tl_to_stop = getStopTLS(stopEdges, net)
1579
numRemoved = removeDeadlockingBidiConstraints(conflicts, tl_to_stop, options.verbose)
1580
numRemoved += checkBidiConsistency(conflicts, options.verbose)
1581
numConflicts -= numRemoved
1582
1583
if numConflicts > 0:
1584
print("Found %s bidi conflicts" % numConflicts)
1585
if numIgnoredConflicts > 0:
1586
print("Ignored %s bidi conflicts" % (numIgnoredConflicts))
1587
return conflicts
1588
1589
1590
def collectBidiConflicts(options, net, vehicleStopRoutes, stop, stopRoute, edgesBefore, arrivals):
1591
if stop.busStop == options.debugStop:
1592
print("findBidiConflicts at stop %s" % options.debugStop)
1593
for (e2Start, e2end) in sorted(arrivals.keys()):
1594
arrivalList = arrivals[(e2Start, e2end)]
1595
nStopArrival = getArrivalSecure(stop)
1596
e1End = getBidiID(net, e2Start)
1597
e1Start = getBidiID(net, e2end)
1598
# time of reaching the end of the conflict section
1599
nArrival = nStopArrival - getTravelTimeToStop(net, e1End, edgesBefore, stop, True)
1600
# we want to find the latest arrival that comes before nArrival (and optionally earlier ones)
1601
arrivalList.sort(reverse=True, key=itemgetter(0))
1602
# print("found oppositeArrivals", [a[0] for a in arrivals])
1603
conflictArrival = None
1604
if options.verbose and stop.busStop == options.debugStop:
1605
print("%s (%s) n: %s %s start: %s end: %s" % (
1606
humanReadableTime(nArrival),
1607
humanReadableTime(nStopArrival),
1608
stop.tripId, stop.vehID, e1Start, e1End))
1609
1610
for pArrival, pStop, sIb, sI2 in arrivalList:
1611
if pArrival >= nArrival:
1612
continue
1613
if conflictArrival:
1614
if options.redundant >= 0:
1615
if conflictArrival - pArrival > options.redundant:
1616
break
1617
else:
1618
break
1619
1620
if pStop.busStop == stop.busStop:
1621
# most likely a small reversal loop. There may
1622
# be some value in keeping the conflict (but
1623
# probably not strictly necessary)
1624
continue
1625
1626
stopRoute2 = vehicleStopRoutes[pStop.vehID]
1627
# signal before vehID enters the conflict section
1628
nSignal = findSignal(net, getEdges(stopRoute, sIb, e1Start, False, noIndex=True), True)
1629
# signal before vehID2 enters the conflict section (in the opposite direction)
1630
pSignal = findSignal(net, getEdges(stopRoute2, sI2, e2Start, False, noIndex=True), True)
1631
1632
if options.verbose and stop.busStop == options.debugStop:
1633
print(" %s (%s) p: %s %s start: %s end: %s" % (
1634
humanReadableTime(pArrival),
1635
humanReadableTime(parseTime(pStop.arrival)),
1636
pStop.tripId, pStop.vehID, e2Start, e2end))
1637
1638
if pStop.vehID != stop.vehID:
1639
if nSignal is None or pSignal is None:
1640
error = ("Ignoring bidi conflict for %s and %s between stops '%s' and '%s'" +
1641
" because no rail signal was found for %s before edge '%s'")
1642
1643
if nSignal is None:
1644
print(error % (stop.prevTripId, pStop.prevTripId, stop.busStop, pStop.busStop,
1645
stop.prevTripId, e1Start), file=sys.stderr)
1646
elif pSignal is None:
1647
print(error % (stop.prevTripId, pStop.prevTripId, stop.busStop, pStop.busStop,
1648
pStop.prevTripId, e2Start), file=sys.stderr)
1649
yield None
1650
continue
1651
1652
if pSignal == nSignal:
1653
error = ("Ignoring bidi conflict for %s and %s between stops '%s' and '%s'" +
1654
" because the found rail signals are the same ('%s')")
1655
print(error % (stop.prevTripId, pStop.prevTripId, stop.busStop, pStop.busStop, pSignal),
1656
file=sys.stderr)
1657
yield None
1658
continue
1659
1660
limit = 1
1661
times = "arrival=%s foeArrival=%s stopArrival=%s foeStopArrival=%s" % (
1662
humanReadableTime(nArrival), humanReadableTime(pArrival),
1663
humanReadableTime(getArrivalSecure(stop)),
1664
humanReadableTime(getArrivalSecure(pStop)))
1665
info = ""
1666
active = not stop.getAttributeSecure(
1667
"invalid", False) and not pStop.getAttributeSecure("invalid", False)
1668
1669
if conflictArrival is None:
1670
conflictArrival = pArrival
1671
1672
# bidi conflicts can profit from additional comments/params
1673
busStop2 = [("busStop2", pStop.busStop)]
1674
# record stops preceding the conflict section for ego and foe vehicle
1675
if sIb > 0:
1676
busStop2.append(("priorStop", stopRoute[sIb - 1][1].busStop))
1677
if sI2 > 0:
1678
busStop2.append(("priorStop2", stopRoute2[sI2 - 1][1].busStop))
1679
1680
conflict = Conflict(stop.prevTripId, pSignal, pStop.prevTripId, limit,
1681
# attributes for adding comments
1682
stop.prevLine, pStop.prevLine,
1683
stop.vehID, pStop.vehID,
1684
times, None,
1685
stop.busStop,
1686
info, active,
1687
busStop2=busStop2)
1688
conflict.signal = nSignal
1689
yield conflict
1690
1691
1692
def checkBidiConsistency(conflicts, verbose):
1693
# if the bidi section between two stops has an intermediate non-bidi section,
1694
# inconsistent constraints may be generated (#12075)
1695
# instead of trying to identify these cases beforehand we filter them out in a post-processing step
1696
1697
tfcMap = defaultdict(list) # (tripId, foeId) -> [conflict, ...]
1698
numRemoved = 0
1699
1700
for signal in sorted(conflicts.keys()):
1701
for c in conflicts[signal]:
1702
busStop2 = c.busStop2[0][1]
1703
key = (c.tripID, c.otherTripID, c.busStop, busStop2)
1704
rkey = (c.otherTripID, c.tripID, busStop2, c.busStop)
1705
if key in tfcMap and verbose:
1706
print("Duplicate conflict between '%s' and '%s' between busStops '%s' and '%s'" % key, file=sys.stderr)
1707
tfcMap[key].append(c)
1708
if rkey in tfcMap:
1709
keyToRemove = None
1710
# decide which of the two conflicts to keep
1711
times = dict([t.split('=') for t in c.conflictTime.split()])
1712
if parseTime(times['foeStopArrival']) < parseTime(times['stopArrival']):
1713
keyToRemove = rkey
1714
else:
1715
keyToRemove = key
1716
1717
for c in tfcMap[keyToRemove]:
1718
numRemoved += 1
1719
conflicts[c.signal].remove(c)
1720
if verbose:
1721
print("Found symmetrical bidi conflict (tripId=%s, foeId=%s, busStop=%s busStop2=%s)." %
1722
keyToRemove)
1723
if not conflicts[c.signal]:
1724
del conflicts[c.signal]
1725
del tfcMap[keyToRemove]
1726
1727
if numRemoved > 0 and verbose:
1728
print("Removed %s symmetrical bidi conflicts" % numRemoved)
1729
1730
return numRemoved
1731
1732
1733
def removeDeadlockingBidiConstraints(conflicts, tl_to_stop, verbose):
1734
"""If there is a non-bidi section in between two single tracks
1735
(like so: A--=B=--C), contradictory constraints may be generated,
1736
where train at A waits for train at C to pass a signal at B, and where
1737
train at C waits for train at A to pass the signal at B.
1738
"""
1739
tfcMap = defaultdict(list)
1740
numRemoved = 0
1741
1742
for signal in sorted(conflicts.keys()):
1743
for c in conflicts[signal]:
1744
other_stop = tl_to_stop.get(c.otherSignal, None)
1745
if not other_stop:
1746
continue
1747
key = (c.tripID, c.otherTripID, other_stop)
1748
rkey = (c.otherTripID, c.tripID, other_stop)
1749
if key in tfcMap and verbose:
1750
print("Deadlock-causing conflict between '%s' and '%s' at busStop '%s'." % key, file=sys.stderr)
1751
tfcMap[key].append(c)
1752
if rkey in tfcMap:
1753
keyToRemove = None
1754
# decide which of the two conflicts to keep
1755
times = dict([t.split("=") for t in c.conflictTime.split()])
1756
if parseTime(times["foeStopArrival"]) < parseTime(times["stopArrival"]):
1757
keyToRemove = rkey
1758
else:
1759
keyToRemove = key
1760
for c in tfcMap[keyToRemove]:
1761
numRemoved += 1
1762
conflicts[c.signal].remove(c)
1763
if verbose:
1764
print("Found bidi conflict causing deadlock (tripId=%s, foeId=%s, busStop=%s)." % keyToRemove)
1765
if not conflicts[c.signal]:
1766
del conflicts[c.signal]
1767
del tfcMap[keyToRemove]
1768
if numRemoved > 0 and verbose:
1769
print("Removed %s deadlock-causing bidi conflicts." % numRemoved)
1770
return numRemoved
1771
1772
1773
def getEdges(stopRoute, index, startEdge, forward, noIndex=False):
1774
"""return all edges along the route starting at the given stop index and startEdge
1775
either going forward or backward
1776
if noIndex is set only the edges are yielded
1777
otherwise the current stop index is yielded as well
1778
"""
1779
endIndex = len(stopRoute) if forward else -1
1780
inc = 1 if forward else -1
1781
while index != endIndex:
1782
edgesBefore, stop = stopRoute[index]
1783
seq = edgesBefore if forward else list(reversed(edgesBefore))
1784
for e in seq:
1785
if startEdge and e != startEdge:
1786
continue
1787
if startEdge:
1788
startEdge = None
1789
if noIndex:
1790
yield e
1791
else:
1792
yield e, index
1793
index += inc
1794
1795
1796
def findDivergence(net, arrivals, backwardGen, forwardGen, stopRoute2, sI2, e2Start, ignoreConflict, visited=None):
1797
e1prev = None
1798
e2prev = None
1799
for (e1, sIb), (e2, sI2b) in zip(backwardGen, forwardGen):
1800
if visited is not None:
1801
visited.append(e2)
1802
if e2 != getBidiID(net, e1):
1803
if e2 == e1prev or ignoreConflict:
1804
# opposite train has a reversal. This leads to a different kind of conflict
1805
return None
1806
# found divergence
1807
edgesBefore, stop2b = stopRoute2[sI2b]
1808
arrival = getArrivalSecure(stop2b)
1809
arrivalConflictEnd = arrival - getTravelTimeToStop(net, e2, edgesBefore, stop2b, False)
1810
# both e2Start and e2Prev are part of the bidi section (e2 is already diverged)
1811
arrivals[(e2Start, e2prev)].append((arrivalConflictEnd, stop2b, sIb, sI2))
1812
return sI2
1813
e1prev = e1
1814
e2prev = e2
1815
return None
1816
1817
1818
def writeConstraint(options, outf, tag, c):
1819
if c.tag is not None:
1820
tag = c.tag
1821
close = "/>"
1822
comment = ""
1823
limit = c.limit + options.limit
1824
commentParams = []
1825
if options.commentLine:
1826
if c.line != "":
1827
comment += "line=%s " % c.line
1828
commentParams.append(("line", c.line))
1829
if c.otherLine != "":
1830
comment += "foeLine=%s " % c.otherLine
1831
commentParams.append(("foeLine", c.otherLine))
1832
if options.commentId:
1833
if c.vehID != c.tripID:
1834
comment += "vehID=%s " % c.vehID
1835
commentParams.append(("vehID", c.vehID))
1836
if c.otherVehID != c.otherTripID:
1837
comment += "foeID=%s " % c.otherVehID
1838
commentParams.append(("foeID", c.otherVehID))
1839
if options.commentSwitch and c.switch is not None:
1840
comment += "switch=%s " % c.switch
1841
commentParams.append(("switch", c.switch))
1842
if options.commentStop:
1843
comment += "busStop=%s " % c.busStop
1844
commentParams.append(("busStop", c.busStop))
1845
if c.busStop2 is not None:
1846
if isinstance(c.busStop2, list):
1847
for k, v in c.busStop2:
1848
comment += "%s=%s " % (k, v)
1849
commentParams.append((k, v))
1850
else:
1851
comment += "busStop2=%s " % c.busStop2
1852
commentParams.append(("busStop2", c.busStop2))
1853
if options.commentTime:
1854
comment += c.conflictTime
1855
for t in c.conflictTime.split():
1856
k, v = t.split('=')
1857
commentParams.append((k, v))
1858
if c.info != "":
1859
comment += "(%s) " % c.info
1860
commentParams.append(("info", c.info))
1861
if comment != "":
1862
if options.commentParams:
1863
close = ">"
1864
comment = ""
1865
for k, v in commentParams:
1866
comment += '\n <param key="%s" value="%s"/>' % (k, v)
1867
comment += '\n </%s>\n' % tag
1868
else:
1869
comment = " <!-- %s -->" % comment
1870
if limit == 1:
1871
limit = ""
1872
else:
1873
limit = ' limit="%s"' % limit
1874
active = ""
1875
if not c.active or options.allInactive:
1876
active = ' active="false"'
1877
1878
outf.write(' <%s tripId="%s" tl="%s" foes="%s"%s%s%s%s\n' % (
1879
tag, c.tripID, c.otherSignal, c.otherTripID, limit, active, close, comment))
1880
1881
1882
def getStopTLS(stopEdges, net):
1883
# Generates a mapping from traffic lights to associated stop locations
1884
tls_to_stop = {}
1885
for key, val in stopEdges.items():
1886
stop = key.split("@")[-1]
1887
node = net.getEdge(val).getFromNode()
1888
if node.getType() == "rail_signal":
1889
tl = net.getTLS(node.getID()).getID()
1890
tls_to_stop[tl] = stop
1891
return tls_to_stop
1892
1893
1894
def main(options):
1895
net = sumolib.net.readNet(options.netFile)
1896
stopEdges, stopEnds = getStopEdges(net, options.addFile)
1897
bidiStops = getBidiStops(options, net, stopEdges)
1898
uniqueRoutes, stopRoutes, stopRoutesBidi, vehicleStopRoutes, departTimes = getStopRoutes(
1899
net, options, stopEdges, stopEnds, bidiStops) # noqa
1900
if options.abortUnordered:
1901
markOvertaken(options, vehicleStopRoutes, stopRoutes)
1902
parkingConflicts, foeParkingInsertionConflicts = updateStartedEnded(
1903
options, net, stopEdges, stopRoutesBidi, vehicleStopRoutes)
1904
1905
mergeSwitches = findMergingSwitches(options, uniqueRoutes, net)
1906
signalTimes = computeSignalTimes(options, net, stopRoutes)
1907
switchRoutes, mergeSignals = findStopsAfterMerge(net, stopRoutes, mergeSwitches)
1908
conflicts, intermediateParkingConflicts = findConflicts(
1909
options, net, switchRoutes, mergeSignals, signalTimes, stopEdges, vehicleStopRoutes)
1910
1911
foeInsertionConflicts = findFoeInsertionConflicts(options, net, stopEdges, stopRoutesBidi, vehicleStopRoutes)
1912
insertionConflicts = findInsertionConflicts(options, net, stopEdges, stopRoutesBidi, vehicleStopRoutes, departTimes)
1913
inactiveInsertionConflicts = defaultdict(list)
1914
if options.writeInactive:
1915
inactiveInsertionConflicts = findInsertionConflicts(options, net, stopEdges, stopRoutesBidi,
1916
vehicleStopRoutes, departTimes, True)
1917
1918
bidiConflicts = findBidiConflicts(options, net, stopEdges, uniqueRoutes,
1919
stopRoutes, vehicleStopRoutes)
1920
1921
signals = sorted(set(list(conflicts.keys())
1922
+ list(foeInsertionConflicts.keys())
1923
+ list(foeParkingInsertionConflicts.keys())
1924
+ list(insertionConflicts.keys())
1925
+ list(parkingConflicts.keys())
1926
+ list(intermediateParkingConflicts.keys())
1927
+ list(bidiConflicts.keys())))
1928
1929
with open(options.out, "w") as outf:
1930
sumolib.writeXMLHeader(outf, "$Id$", "additional", options=options) # noqa
1931
for signal in signals:
1932
outf.write(' <railSignalConstraints id="%s">\n' % signal)
1933
for conflict in conflicts[signal]:
1934
writeConstraint(options, outf, "predecessor", conflict)
1935
for conflict in (foeInsertionConflicts[signal] +
1936
foeParkingInsertionConflicts[signal]):
1937
writeConstraint(options, outf, "foeInsertion", conflict)
1938
for conflict in (insertionConflicts[signal] +
1939
inactiveInsertionConflicts[signal] +
1940
parkingConflicts[signal] +
1941
intermediateParkingConflicts[signal]):
1942
writeConstraint(options, outf, "insertionPredecessor", conflict)
1943
for conflict in bidiConflicts[signal]:
1944
writeConstraint(options, outf, "bidiPredecessor", conflict)
1945
outf.write(' </railSignalConstraints>\n')
1946
outf.write('</additional>\n')
1947
1948
1949
if __name__ == "__main__":
1950
main(get_options())
1951
1952