Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/drt/drtOnline.py
169674 views
1
#!/usr/bin/env python
2
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3
# Copyright (C) 2007-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 drtOnline.py
15
# @author Giuliana Armellini
16
# @author Mirko Barthauer
17
# @date 2021-02-15
18
19
"""
20
Simulate Demand Responsive Transport via TraCi
21
Track progress https://github.com/eclipse/sumo/issues/8256
22
"""
23
24
from __future__ import print_function
25
import os
26
import sys
27
from itertools import combinations
28
import subprocess
29
import shutil
30
31
import pulp as pl
32
import darpSolvers
33
34
if 'SUMO_HOME' in os.environ:
35
sys.path.append(os.path.join(os.environ['SUMO_HOME'], 'tools'))
36
from sumolib import checkBinary # noqa
37
from sumolib.xml import parse_fast_nested # noqa
38
from sumolib.options import ArgumentParser # noqa
39
import traci # noqa
40
findRoute = traci.simulation.findRoute
41
42
43
def initOptions():
44
ap = ArgumentParser()
45
46
ap.add_argument("-n", "--network", dest="network", category="input", type=ArgumentParser.net_file, metavar="FILE",
47
help="SUMO network file")
48
ap.add_argument("-r", "--reservations", category="input", type=ArgumentParser.file, metavar="FILE",
49
help="File with reservations (persons)")
50
ap.add_argument("--taxis", category="input", type=ArgumentParser.file, metavar="FILE",
51
help="File with drt vehicles")
52
ap.add_argument("--sumocfg", category="input", type=ArgumentParser.file, metavar="FILE",
53
help="Sumo configuration file")
54
ap.add_argument("-s", dest="sumo", default="sumo", type=str, choices=('sumo', 'sumo-gui'),
55
help="Run with sumo (default) or sumo-gui")
56
ap.add_argument("-g", dest="gui_settings", category="input", type=ArgumentParser.net_file, metavar="FILE",
57
help="Load visualization settings from FILE")
58
ap.add_argument("-o", dest="output", default='tripinfos.xml', category="output", type=ArgumentParser.net_file,
59
help="Name of output file")
60
ap.add_argument("--darp-solver", default='exhaustive_search', type=str,
61
help="Method to solve the DARP problem. Available: exhaustive_search and simple_rerouting")
62
ap.add_argument("--rtv-time", type=float, default=5,
63
help="Timeout for exhaustive search (default 5 seconds)")
64
ap.add_argument("--ilp-time", type=float, default=5,
65
help="Timeout for ILP solver (default 5 seconds)")
66
ap.add_argument("--c-ko", type=int, default=1000000000,
67
help="Cost of ignoring a reservation")
68
ap.add_argument("--cost-per-trip", type=int, default=600, help="Cost to avoid using multiple vehicles"
69
" if the travel time of trips is similar (default 600 seconds)")
70
ap.add_argument("--drf", dest="drf", type=float, default=2, help="Factor by which the DRT travel time "
71
"should not exceed the one of a direct connection (default 2)")
72
ap.add_argument("--drf-min", type=ArgumentParser.time, default=600, help="Minimum time difference allowed "
73
"between DRT travel time and direct connection for the cases of short trips (default 600 seconds)")
74
ap.add_argument("--max-wait", type=ArgumentParser.time, default=900,
75
help="Maximum waiting time for pickup (default 900 seconds)")
76
ap.add_argument("--max-processing", type=int,
77
help="Maximum number of attempts to process a request (default unlimited)")
78
ap.add_argument("--sim-step", type=int, default=30,
79
help="Step time to collect new reservations (default 30 seconds)")
80
ap.add_argument("--end-time", type=ArgumentParser.time, default=90000,
81
help="Maximum simulation time to close Traci (default 90000 sec - 25h)")
82
ap.add_argument("--routing-algorithm", default='dijkstra', choices=('dijkstra', 'astar', 'CH', 'CHWrapper'),
83
help="Algorithm for shortest path routing. Support: dijkstra (default), astar, CH and CHWrapper")
84
ap.add_argument("--routing-mode", type=int, default=0, help="Mode for shortest path routing. Support: 0 (default) "
85
"for routing with loaded or default speeds and 1 for routing with averaged historical speeds")
86
ap.add_argument("--dua-times", action='store_true',
87
help="Calculate travel time between edges with duarouter")
88
ap.add_argument("--tracefile", category="output", type=ArgumentParser.file,
89
help="log traci commands to the given FILE")
90
ap.add_argument("--tracegetters", action='store_true',
91
help="include get-methods in tracefile")
92
ap.add_argument("-v", "--verbose", action='store_true')
93
# mainly useful for reproducible tests
94
ap.add_argument("--seed", type=int, help="Set a random seed for the ILP solver")
95
96
return ap
97
98
99
def find_dua_times(options):
100
"""
101
Get all combinations between start and end reservation edges and calculates
102
the travel time between all combinations with duarouter in an empty net.
103
"""
104
105
edge_pair_time = {}
106
os.mkdir('temp_dua')
107
108
# define trips between all edge combinations
109
route_edges = []
110
for person, reservation in parse_fast_nested(options.reservations,
111
"person", "depart", "ride",
112
("from", "to", "lines")):
113
route_edges.extend((reservation.attr_from, reservation.to))
114
115
combination_edges = combinations(set(route_edges), 2)
116
with open("temp_dua/dua_file.xml", "w+") as dua_file:
117
dua_file.write("<routes>\n")
118
for comb_edges in list(combination_edges):
119
dua_file.write(' <trip id="%s_%s" depart="0" from="%s" to="%s"/>\n'
120
% (comb_edges[0], comb_edges[1], comb_edges[0], comb_edges[1]))
121
dua_file.write(' <trip id="%s_%s" depart="0" from="%s" to="%s"/>\n'
122
% (comb_edges[1], comb_edges[0], comb_edges[1], comb_edges[0]))
123
dua_file.write("</routes>\n")
124
125
# run duarouter:
126
duarouter = checkBinary('duarouter')
127
128
subprocess.call([duarouter, "-n", options.network, "--route-files",
129
"temp_dua/dua_file.xml", "-o", "temp_dua/dua_output.xml",
130
"--ignore-errors", "true", "--no-warnings", "true",
131
"--bulk-routing", "true"])
132
133
# parse travel time between edges
134
with open("edges_pair_graph.xml", "w+") as pair_file:
135
for trip, route in parse_fast_nested("temp_dua/dua_output.alt.xml",
136
"vehicle", "id", "route", "cost",
137
optional=True):
138
if route.cost:
139
edge_pair_time[trip.id] = float(route.cost)
140
pair_file.write('<pair id="%s" cost="%s"/>\n'
141
% (trip.id, float(route.cost)))
142
143
# remove extra dua files
144
shutil.rmtree('temp_dua')
145
146
return edge_pair_time
147
148
149
def ilp_solve(options, veh_num, res_num, costs, veh_constraints,
150
res_constraints):
151
"""
152
Solves the integer linear programming to define the best routes for each
153
vehicle. Only implemented for problems with multiple vehicles.
154
"""
155
# founds the combination of trips that minimize the costs function
156
157
# req_costs = [reservation.cost for reservation in reservations] # TODO to implement req with diff costs/priorities
158
order_trips = costs.keys()
159
160
ILP_result = []
161
162
# Create the 'prob' variable to contain the problem data
163
prob = pl.LpProblem("DARP", pl.LpMinimize)
164
165
# 'Trips_vars' dict with the referenced Variables (all possible trips)
166
Trips_vars = pl.LpVariable.dicts("Trip", order_trips, cat='Binary')
167
168
# add objective function
169
# solution cost = sum(cost of each trip) -
170
# sum(reservation rejection cost * reservations served in trip)
171
prob += pl.lpSum([costs[i] * Trips_vars[i] for i in order_trips]) - \
172
options.c_ko * pl.lpSum([sum(res_constraints[i]) * Trips_vars[i]
173
for i in order_trips]), "Trips_travel_time"
174
175
# add constraints
176
for index in range(veh_num):
177
prob += pl.lpSum([veh_constraints[i][index] * Trips_vars[i]
178
for i in order_trips]) <= 1, "Max_1_trip_per_vehicle_%s" % index
179
180
for index in range(res_num):
181
prob += pl.lpSum([res_constraints[i][index] * Trips_vars[i]
182
for i in order_trips]) <= 1, "Max_1_trip_per_reservation_%s" % index
183
184
prob += pl.lpSum([sum(veh_constraints[i]) * Trips_vars[i]
185
for i in order_trips]) >= 1, "Assing_at_least_one_vehicle"
186
187
# The problem is solved using PuLP's Solver choice
188
cbc_opts = ["RandomS %s" % options.seed] if options.seed else None
189
try:
190
prob.solve(pl.PULP_CBC_CMD(msg=0, timeLimit=options.ilp_time, options=cbc_opts))
191
except pl.apis.core.PulpSolverError:
192
prob.solve(pl.COIN_CMD(msg=0, timeLimit=options.ilp_time, path="/usr/bin/cbc", options=cbc_opts))
193
194
if pl.LpStatus[prob.status] != 'Optimal':
195
sys.exit("No optimal solution found: %s" % pl.LpStatus[prob.status])
196
else: # if optimal solution was found
197
for variable in prob.variables():
198
if variable.varValue == 1:
199
result = variable.name.split("Trip_")[1]
200
ILP_result.append(result)
201
202
return ILP_result
203
204
205
def main():
206
# read inputs
207
ap = initOptions()
208
options = ap.parse_args()
209
210
res_all = {}
211
212
if options.sumo == 'sumo':
213
SUMO = checkBinary('sumo')
214
else:
215
SUMO = checkBinary('sumo-gui')
216
217
# start traci
218
if options.sumocfg:
219
run_traci = [SUMO, "-c", options.sumocfg,
220
'--tripinfo-output.write-unfinished',
221
'--routing-algorithm', options.routing_algorithm]
222
else:
223
run_traci = [SUMO, '--net-file', '%s' % options.network, '-r',
224
'%s,%s' % (options.reservations, options.taxis), '-l',
225
'log.txt', '--device.taxi.dispatch-algorithm', 'traci',
226
'--tripinfo-output', '%s' % options.output,
227
'--tripinfo-output.write-unfinished',
228
'--routing-algorithm', options.routing_algorithm,
229
'--stop-output', 'stops_%s' % options.output]
230
if options.gui_settings:
231
run_traci.extend(['-g', '%s' % options.gui_settings])
232
233
if options.dua_times:
234
if options.verbose:
235
print('Calculate travel time between edges with duarouter')
236
if not options.reservations:
237
sys.exit("please specify the reservation file with the option '--reservations'")
238
if not options.network:
239
sys.exit("please specify the sumo network file with the option '--network'")
240
pairs_dua_times = find_dua_times(options)
241
else:
242
pairs_dua_times = {}
243
244
traci.start(run_traci, traceFile=options.tracefile, traceGetters=options.tracegetters)
245
246
# execute the TraCI control loop
247
step = traci.simulation.getTime() + 10
248
veh_type = None
249
rerouting = True
250
while rerouting:
251
traci.simulationStep(step)
252
253
# TODO ticket #8385
254
if not traci.vehicle.getTaxiFleet(-1) and step < options.end_time:
255
step += options.sim_step
256
continue
257
258
# get vType and its parameters for route calculation
259
if not veh_type:
260
fleet = traci.vehicle.getTaxiFleet(-1)
261
veh_type = traci.vehicle.getTypeID(fleet[0])
262
veh_time_pickup = float(traci.vehicle.getParameter(fleet[0],
263
'device.taxi.pickUpDuration'))
264
veh_time_dropoff = float(traci.vehicle.getParameter(fleet[0],
265
'device.taxi.dropOffDuration'))
266
267
# get new reservations
268
res_id_new = []
269
for res in traci.person.getTaxiReservations(1):
270
271
# search direct travel time
272
direct = pairs_dua_times.get("%s_%s" % (res.fromEdge, res.toEdge))
273
if direct is None:
274
direct = int(findRoute(res.fromEdge, res.toEdge, veh_type,
275
res.depart, routingMode=options.routing_mode).travelTime)
276
277
# add new reservation attributes
278
setattr(res, 'direct', direct) # direct travel time
279
setattr(res, 'vehicle', False) # id of assigned vehicle
280
setattr(res, 'delay', 0) # real pick up time - assigned time
281
282
# read extra attributes
283
person_id = res.persons[0]
284
pickup_earliest = traci.person.getParameter(person_id,
285
"pickup_earliest")
286
if pickup_earliest:
287
pickup_earliest = float(pickup_earliest)
288
dropoff_latest = traci.person.getParameter(person_id,
289
"dropoff_latest")
290
if dropoff_latest:
291
dropoff_latest = float(dropoff_latest)
292
max_waiting = traci.person.getParameter(person_id, "max_waiting")
293
if max_waiting:
294
max_waiting = float(max_waiting)
295
296
# calculates time windows
297
if not max_waiting:
298
# take global value
299
max_waiting = options.max_wait
300
if pickup_earliest and dropoff_latest:
301
# set latest pickup based on waiting time or latest drop off
302
pickup_latest = min(pickup_earliest + max_waiting,
303
dropoff_latest - direct)
304
# if drop off time given, set time window based on waiting time
305
dropoff_earliest = max(pickup_earliest + direct,
306
dropoff_latest - max_waiting)
307
elif dropoff_latest:
308
# if latest drop off given, calculate pickup window based
309
# on max. travel time with drf
310
if res.direct*options.drf < options.drf_min:
311
pickup_earliest = max(res.depart,
312
dropoff_latest - options.drf_min)
313
else:
314
pickup_earliest = max(res.depart,
315
dropoff_latest - direct*options.drf)
316
pickup_latest = max(pickup_earliest, dropoff_latest - direct)
317
dropoff_earliest = max(pickup_earliest + direct,
318
dropoff_latest - max_waiting)
319
else:
320
if not pickup_earliest:
321
# if no time was given
322
pickup_earliest = res.depart
323
# set earliest drop off based on pickup and max. travel time
324
dropoff_earliest = pickup_earliest + direct
325
# check if min travel time or drf must be applied
326
if res.direct*options.drf < options.drf_min:
327
dropoff_latest = pickup_earliest + direct + options.drf_min
328
else:
329
dropoff_latest = pickup_earliest + direct*options.drf
330
# set latest pickup based on waiting time
331
pickup_latest = min(dropoff_latest - direct,
332
pickup_earliest + max_waiting)
333
334
# add time window attributes
335
setattr(res, 'tw_pickup', [pickup_earliest, pickup_latest])
336
setattr(res, 'tw_dropoff', [dropoff_earliest, dropoff_latest])
337
338
# time out for request processing
339
if options.max_processing:
340
setattr(res, 'max_processing',
341
step + options.max_processing*options.sim_step)
342
else:
343
setattr(res, 'max_processing', pickup_latest+options.sim_step)
344
345
# add reservation id to new reservations
346
res_id_new.append(res.id)
347
# add reservation object to list
348
res_all[res.id] = res
349
350
# find unassigned reservations and
351
# remove reservations which have exceeded the processing time
352
res_id_unassigned = []
353
res_id_proc_exceeded = []
354
for res_key, res_values in res_all.items():
355
if not res_values.vehicle:
356
if step >= res_values.max_processing:
357
res_id_proc_exceeded.append(res_key)
358
print("\nProcessing time for reservation %s -person %s- was exceeded. Reservation can not be served" % (res_key, res_values.persons)) # noqa
359
for person in res_values.persons:
360
traci.person.removeStages(person)
361
else:
362
res_id_unassigned.append(res_key)
363
364
# remove reservations
365
[res_all.pop(key) for key in res_id_proc_exceeded]
366
367
# if reservations pending
368
if res_id_unassigned:
369
if options.verbose:
370
print('\nRun dispatcher')
371
if res_id_new:
372
print('New reservations:', sorted(res_id_new))
373
print('Pending reservations:',
374
sorted(set(res_id_unassigned)-set(res_id_new)))
375
376
# get fleet
377
fleet = traci.vehicle.getTaxiFleet(-1)
378
if set(fleet) != set(fleet) & set(traci.vehicle.getIDList()): # TODO manage teleports
379
print("\nVehicle %s is being teleported, skip to next step" %
380
(set(fleet) - set(traci.vehicle.getIDList())))
381
step += options.sim_step
382
continue # if a vehicle is being teleported skip to next step
383
384
# find vehicle in same edge, avoid calculating same routes twice
385
veh_edges = {}
386
for veh_id in fleet:
387
if traci.vehicle.getRoadID(veh_id).startswith(':'):
388
# avoid routing error when in intersection TODO #5829
389
edge_index = traci.vehicle.getRouteIndex(veh_id) + 1
390
veh_edge = traci.vehicle.getRoute(veh_id)[edge_index]
391
else:
392
veh_edge = traci.vehicle.getRoadID(veh_id)
393
if veh_edges.get(veh_edge) is not None:
394
veh_edges[veh_edge].append(veh_id)
395
else:
396
veh_edges[veh_edge] = [veh_id]
397
398
# remove reservations already served
399
res_id_current = [res.id for res in traci.person.getTaxiReservations(0)]
400
res_id_served = list(set(res_all.keys()) - set(res_id_current))
401
[res_all.pop(key) for key in res_id_served]
402
403
# reservations already picked up
404
res_id_picked = [res.id for res in traci.person.getTaxiReservations(8)]
405
406
# call DARP solver to find the best routes
407
if options.verbose:
408
print('Solve DARP with %s' % options.darp_solver)
409
410
darp_solution = darpSolvers.main(options, step, fleet, veh_type,
411
veh_time_pickup, veh_time_dropoff,
412
res_all, res_id_new,
413
res_id_unassigned, res_id_picked,
414
veh_edges,
415
pairs_dua_times)
416
routes, ilp_res_cons, exact_sol = darp_solution
417
418
if len(routes) == 0:
419
step += options.sim_step
420
continue # if no routes found
421
422
elif ilp_res_cons is None:
423
# if DARP solver found optimal routes, this is, no ILP needed
424
best_routes = list(routes.keys())
425
426
else:
427
# if DARP solver not optimized routes, run Integer Linear
428
# Programming with pulp
429
veh_constraints = {}
430
res_constraints = {}
431
costs = {}
432
trips = list(sorted(routes.keys())) # trips for parsing ILP solution
433
434
# add bonus_cost to trip cost (makes trips with more served
435
# reservations cheaper than splitting the reservations to more
436
# vehicles with smaller trips if both strategies would yield
437
# a similar cost)
438
for idx, trip_id in enumerate(trips):
439
# routes[route] = [travel_time, veh_bin, res_bin, value]
440
# TODO specific cost for vehicle can be consider here
441
bonus_cost = (sum(routes[trip_id][2]) + 1) * options.cost_per_trip
442
# generate dict with costs
443
costs.update({idx: routes[trip_id][0] + bonus_cost})
444
# generate dict with vehicle used in the trip
445
veh_constraints.update({idx: routes[trip_id][1]})
446
# generate dict with served reservations in the trip
447
res_constraints.update({idx: routes[trip_id][2]})
448
449
if options.verbose:
450
print('Solve ILP')
451
ilp_result = ilp_solve(options, len(fleet), len(ilp_res_cons),
452
costs, veh_constraints, res_constraints)
453
454
# parse ILP result
455
best_routes = [trips[int(route_index)]
456
for route_index in ilp_result]
457
458
# assign routes to vehicles
459
for route_id in sorted(best_routes):
460
stops = route_id.replace('y', '').replace('z', '').split("_")
461
veh_id = stops[0]
462
# first check if new route is better than the current one
463
current_route = []
464
if traci.vehicle.getStops(veh_id):
465
for taxi_stop in traci.vehicle.getStops(veh_id):
466
next_act = taxi_stop.actType.split(",")[0].split(" ")[0]
467
if not next_act:
468
# vehicle doesn't have a current route
469
continue
470
next_id = taxi_stop.actType.split(",")[0].split(" ")[-1][1:-1]
471
if next_act == 'pickup' and next_id in res_id_picked:
472
# person already picked up, consider next stop
473
continue
474
elif next_act == 'dropOff' and next_id not in res_all.keys():
475
# person already dropped off, consider next stop
476
continue
477
# get reservations served at each stop
478
sub_stops = taxi_stop.actType.split(",")
479
# if more than 1 reservation in stop
480
for sub_stop in sub_stops:
481
current_route.append(sub_stop.split(" ")[2][1:-1])
482
if current_route == stops[1:]:
483
# route is the same
484
continue
485
elif (set(current_route) == set(stops[1:]) and
486
len(current_route) == len(stops[1:])):
487
# if route serve same reservations and have the same stops
488
# get travel time of current route
489
tt_current_route = step
490
edges = [taxi_stop.lane.split("_")[0] for taxi_stop
491
in traci.vehicle.getStops(veh_id)]
492
stop_types = [taxi_stop.actType for taxi_stop
493
in traci.vehicle.getStops(veh_id)]
494
# add current edge to list
495
if traci.vehicle.getRoadID(veh_id).startswith(':'):
496
# avoid routing error when in intersection TODO #5829
497
edge_index = traci.vehicle.getRouteIndex(veh_id) + 1
498
veh_edge = traci.vehicle.getRoute(veh_id)[edge_index]
499
else:
500
veh_edge = traci.vehicle.getRoadID(veh_id)
501
502
edges.insert(0, veh_edge)
503
# calculate travel time
504
for idx, edge in enumerate(edges[:-1]):
505
tt_pair = pairs_dua_times.get("%s_%s" % (edge,
506
edges[idx+1]))
507
if tt_pair is None:
508
tt_pair = int(findRoute(edge, edges[idx+1],
509
veh_type, step, routingMode=options.routing_mode).travelTime)
510
511
if 'pickup' in stop_types[idx]:
512
tt_current_route += tt_pair + veh_time_pickup
513
else:
514
tt_current_route += tt_pair + veh_time_dropoff
515
# get travel time of the new route
516
tt_new_route = routes[route_id][0]
517
if tt_new_route >= tt_current_route:
518
continue # current route better than new found
519
if options.verbose:
520
print('Dispatch:', route_id)
521
traci.vehicle.dispatchTaxi(veh_id, stops[1:])
522
# assign vehicle to reservations
523
# TODO to avoid major changes in the pick-up time when assigning new passengers,
524
# tw_pickup should be updated, whit some constant X seconds, e.g. 10 Minutes
525
for res_id in set(stops[1:]):
526
res = res_all[res_id]
527
res.vehicle = veh_id
528
529
# TODO ticket #8385
530
if step > options.end_time:
531
rerouting = False
532
533
step += options.sim_step
534
535
if all(exact_sol):
536
print('\nExact solution found.')
537
else:
538
print('\nApproximate solution found.')
539
print('DRT simulation ended')
540
traci.close()
541
542
543
if __name__ == "__main__":
544
main()
545
546