"""
Simulate Demand Responsive Transport via TraCi
Track progress https://github.com/eclipse/sumo/issues/8256
"""
from __future__ import print_function
import os
import sys
from itertools import combinations
import subprocess
import shutil
import pulp as pl
import darpSolvers
if 'SUMO_HOME' in os.environ:
sys.path.append(os.path.join(os.environ['SUMO_HOME'], 'tools'))
from sumolib import checkBinary
from sumolib.xml import parse_fast_nested
from sumolib.options import ArgumentParser
import traci
findRoute = traci.simulation.findRoute
def initOptions():
ap = ArgumentParser()
ap.add_argument("-n", "--network", dest="network", category="input", type=ArgumentParser.net_file, metavar="FILE",
help="SUMO network file")
ap.add_argument("-r", "--reservations", category="input", type=ArgumentParser.file, metavar="FILE",
help="File with reservations (persons)")
ap.add_argument("--taxis", category="input", type=ArgumentParser.file, metavar="FILE",
help="File with drt vehicles")
ap.add_argument("--sumocfg", category="input", type=ArgumentParser.file, metavar="FILE",
help="Sumo configuration file")
ap.add_argument("-s", dest="sumo", default="sumo", type=str, choices=('sumo', 'sumo-gui'),
help="Run with sumo (default) or sumo-gui")
ap.add_argument("-g", dest="gui_settings", category="input", type=ArgumentParser.net_file, metavar="FILE",
help="Load visualization settings from FILE")
ap.add_argument("-o", dest="output", default='tripinfos.xml', category="output", type=ArgumentParser.net_file,
help="Name of output file")
ap.add_argument("--darp-solver", default='exhaustive_search', type=str,
help="Method to solve the DARP problem. Available: exhaustive_search and simple_rerouting")
ap.add_argument("--rtv-time", type=float, default=5,
help="Timeout for exhaustive search (default 5 seconds)")
ap.add_argument("--ilp-time", type=float, default=5,
help="Timeout for ILP solver (default 5 seconds)")
ap.add_argument("--c-ko", type=int, default=1000000000,
help="Cost of ignoring a reservation")
ap.add_argument("--cost-per-trip", type=int, default=600, help="Cost to avoid using multiple vehicles"
" if the travel time of trips is similar (default 600 seconds)")
ap.add_argument("--drf", dest="drf", type=float, default=2, help="Factor by which the DRT travel time "
"should not exceed the one of a direct connection (default 2)")
ap.add_argument("--drf-min", type=ArgumentParser.time, default=600, help="Minimum time difference allowed "
"between DRT travel time and direct connection for the cases of short trips (default 600 seconds)")
ap.add_argument("--max-wait", type=ArgumentParser.time, default=900,
help="Maximum waiting time for pickup (default 900 seconds)")
ap.add_argument("--max-processing", type=int,
help="Maximum number of attempts to process a request (default unlimited)")
ap.add_argument("--sim-step", type=int, default=30,
help="Step time to collect new reservations (default 30 seconds)")
ap.add_argument("--end-time", type=ArgumentParser.time, default=90000,
help="Maximum simulation time to close Traci (default 90000 sec - 25h)")
ap.add_argument("--routing-algorithm", default='dijkstra', choices=('dijkstra', 'astar', 'CH', 'CHWrapper'),
help="Algorithm for shortest path routing. Support: dijkstra (default), astar, CH and CHWrapper")
ap.add_argument("--routing-mode", type=int, default=0, help="Mode for shortest path routing. Support: 0 (default) "
"for routing with loaded or default speeds and 1 for routing with averaged historical speeds")
ap.add_argument("--dua-times", action='store_true',
help="Calculate travel time between edges with duarouter")
ap.add_argument("--tracefile", category="output", type=ArgumentParser.file,
help="log traci commands to the given FILE")
ap.add_argument("--tracegetters", action='store_true',
help="include get-methods in tracefile")
ap.add_argument("-v", "--verbose", action='store_true')
ap.add_argument("--seed", type=int, help="Set a random seed for the ILP solver")
return ap
def find_dua_times(options):
"""
Get all combinations between start and end reservation edges and calculates
the travel time between all combinations with duarouter in an empty net.
"""
edge_pair_time = {}
os.mkdir('temp_dua')
route_edges = []
for person, reservation in parse_fast_nested(options.reservations,
"person", "depart", "ride",
("from", "to", "lines")):
route_edges.extend((reservation.attr_from, reservation.to))
combination_edges = combinations(set(route_edges), 2)
with open("temp_dua/dua_file.xml", "w+") as dua_file:
dua_file.write("<routes>\n")
for comb_edges in list(combination_edges):
dua_file.write(' <trip id="%s_%s" depart="0" from="%s" to="%s"/>\n'
% (comb_edges[0], comb_edges[1], comb_edges[0], comb_edges[1]))
dua_file.write(' <trip id="%s_%s" depart="0" from="%s" to="%s"/>\n'
% (comb_edges[1], comb_edges[0], comb_edges[1], comb_edges[0]))
dua_file.write("</routes>\n")
duarouter = checkBinary('duarouter')
subprocess.call([duarouter, "-n", options.network, "--route-files",
"temp_dua/dua_file.xml", "-o", "temp_dua/dua_output.xml",
"--ignore-errors", "true", "--no-warnings", "true",
"--bulk-routing", "true"])
with open("edges_pair_graph.xml", "w+") as pair_file:
for trip, route in parse_fast_nested("temp_dua/dua_output.alt.xml",
"vehicle", "id", "route", "cost",
optional=True):
if route.cost:
edge_pair_time[trip.id] = float(route.cost)
pair_file.write('<pair id="%s" cost="%s"/>\n'
% (trip.id, float(route.cost)))
shutil.rmtree('temp_dua')
return edge_pair_time
def ilp_solve(options, veh_num, res_num, costs, veh_constraints,
res_constraints):
"""
Solves the integer linear programming to define the best routes for each
vehicle. Only implemented for problems with multiple vehicles.
"""
order_trips = costs.keys()
ILP_result = []
prob = pl.LpProblem("DARP", pl.LpMinimize)
Trips_vars = pl.LpVariable.dicts("Trip", order_trips, cat='Binary')
prob += pl.lpSum([costs[i] * Trips_vars[i] for i in order_trips]) - \
options.c_ko * pl.lpSum([sum(res_constraints[i]) * Trips_vars[i]
for i in order_trips]), "Trips_travel_time"
for index in range(veh_num):
prob += pl.lpSum([veh_constraints[i][index] * Trips_vars[i]
for i in order_trips]) <= 1, "Max_1_trip_per_vehicle_%s" % index
for index in range(res_num):
prob += pl.lpSum([res_constraints[i][index] * Trips_vars[i]
for i in order_trips]) <= 1, "Max_1_trip_per_reservation_%s" % index
prob += pl.lpSum([sum(veh_constraints[i]) * Trips_vars[i]
for i in order_trips]) >= 1, "Assing_at_least_one_vehicle"
cbc_opts = ["RandomS %s" % options.seed] if options.seed else None
try:
prob.solve(pl.PULP_CBC_CMD(msg=0, timeLimit=options.ilp_time, options=cbc_opts))
except pl.apis.core.PulpSolverError:
prob.solve(pl.COIN_CMD(msg=0, timeLimit=options.ilp_time, path="/usr/bin/cbc", options=cbc_opts))
if pl.LpStatus[prob.status] != 'Optimal':
sys.exit("No optimal solution found: %s" % pl.LpStatus[prob.status])
else:
for variable in prob.variables():
if variable.varValue == 1:
result = variable.name.split("Trip_")[1]
ILP_result.append(result)
return ILP_result
def main():
ap = initOptions()
options = ap.parse_args()
res_all = {}
if options.sumo == 'sumo':
SUMO = checkBinary('sumo')
else:
SUMO = checkBinary('sumo-gui')
if options.sumocfg:
run_traci = [SUMO, "-c", options.sumocfg,
'--tripinfo-output.write-unfinished',
'--routing-algorithm', options.routing_algorithm]
else:
run_traci = [SUMO, '--net-file', '%s' % options.network, '-r',
'%s,%s' % (options.reservations, options.taxis), '-l',
'log.txt', '--device.taxi.dispatch-algorithm', 'traci',
'--tripinfo-output', '%s' % options.output,
'--tripinfo-output.write-unfinished',
'--routing-algorithm', options.routing_algorithm,
'--stop-output', 'stops_%s' % options.output]
if options.gui_settings:
run_traci.extend(['-g', '%s' % options.gui_settings])
if options.dua_times:
if options.verbose:
print('Calculate travel time between edges with duarouter')
if not options.reservations:
sys.exit("please specify the reservation file with the option '--reservations'")
if not options.network:
sys.exit("please specify the sumo network file with the option '--network'")
pairs_dua_times = find_dua_times(options)
else:
pairs_dua_times = {}
traci.start(run_traci, traceFile=options.tracefile, traceGetters=options.tracegetters)
step = traci.simulation.getTime() + 10
veh_type = None
rerouting = True
while rerouting:
traci.simulationStep(step)
if not traci.vehicle.getTaxiFleet(-1) and step < options.end_time:
step += options.sim_step
continue
if not veh_type:
fleet = traci.vehicle.getTaxiFleet(-1)
veh_type = traci.vehicle.getTypeID(fleet[0])
veh_time_pickup = float(traci.vehicle.getParameter(fleet[0],
'device.taxi.pickUpDuration'))
veh_time_dropoff = float(traci.vehicle.getParameter(fleet[0],
'device.taxi.dropOffDuration'))
res_id_new = []
for res in traci.person.getTaxiReservations(1):
direct = pairs_dua_times.get("%s_%s" % (res.fromEdge, res.toEdge))
if direct is None:
direct = int(findRoute(res.fromEdge, res.toEdge, veh_type,
res.depart, routingMode=options.routing_mode).travelTime)
setattr(res, 'direct', direct)
setattr(res, 'vehicle', False)
setattr(res, 'delay', 0)
person_id = res.persons[0]
pickup_earliest = traci.person.getParameter(person_id,
"pickup_earliest")
if pickup_earliest:
pickup_earliest = float(pickup_earliest)
dropoff_latest = traci.person.getParameter(person_id,
"dropoff_latest")
if dropoff_latest:
dropoff_latest = float(dropoff_latest)
max_waiting = traci.person.getParameter(person_id, "max_waiting")
if max_waiting:
max_waiting = float(max_waiting)
if not max_waiting:
max_waiting = options.max_wait
if pickup_earliest and dropoff_latest:
pickup_latest = min(pickup_earliest + max_waiting,
dropoff_latest - direct)
dropoff_earliest = max(pickup_earliest + direct,
dropoff_latest - max_waiting)
elif dropoff_latest:
if res.direct*options.drf < options.drf_min:
pickup_earliest = max(res.depart,
dropoff_latest - options.drf_min)
else:
pickup_earliest = max(res.depart,
dropoff_latest - direct*options.drf)
pickup_latest = max(pickup_earliest, dropoff_latest - direct)
dropoff_earliest = max(pickup_earliest + direct,
dropoff_latest - max_waiting)
else:
if not pickup_earliest:
pickup_earliest = res.depart
dropoff_earliest = pickup_earliest + direct
if res.direct*options.drf < options.drf_min:
dropoff_latest = pickup_earliest + direct + options.drf_min
else:
dropoff_latest = pickup_earliest + direct*options.drf
pickup_latest = min(dropoff_latest - direct,
pickup_earliest + max_waiting)
setattr(res, 'tw_pickup', [pickup_earliest, pickup_latest])
setattr(res, 'tw_dropoff', [dropoff_earliest, dropoff_latest])
if options.max_processing:
setattr(res, 'max_processing',
step + options.max_processing*options.sim_step)
else:
setattr(res, 'max_processing', pickup_latest+options.sim_step)
res_id_new.append(res.id)
res_all[res.id] = res
res_id_unassigned = []
res_id_proc_exceeded = []
for res_key, res_values in res_all.items():
if not res_values.vehicle:
if step >= res_values.max_processing:
res_id_proc_exceeded.append(res_key)
print("\nProcessing time for reservation %s -person %s- was exceeded. Reservation can not be served" % (res_key, res_values.persons))
for person in res_values.persons:
traci.person.removeStages(person)
else:
res_id_unassigned.append(res_key)
[res_all.pop(key) for key in res_id_proc_exceeded]
if res_id_unassigned:
if options.verbose:
print('\nRun dispatcher')
if res_id_new:
print('New reservations:', sorted(res_id_new))
print('Pending reservations:',
sorted(set(res_id_unassigned)-set(res_id_new)))
fleet = traci.vehicle.getTaxiFleet(-1)
if set(fleet) != set(fleet) & set(traci.vehicle.getIDList()):
print("\nVehicle %s is being teleported, skip to next step" %
(set(fleet) - set(traci.vehicle.getIDList())))
step += options.sim_step
continue
veh_edges = {}
for veh_id in fleet:
if traci.vehicle.getRoadID(veh_id).startswith(':'):
edge_index = traci.vehicle.getRouteIndex(veh_id) + 1
veh_edge = traci.vehicle.getRoute(veh_id)[edge_index]
else:
veh_edge = traci.vehicle.getRoadID(veh_id)
if veh_edges.get(veh_edge) is not None:
veh_edges[veh_edge].append(veh_id)
else:
veh_edges[veh_edge] = [veh_id]
res_id_current = [res.id for res in traci.person.getTaxiReservations(0)]
res_id_served = list(set(res_all.keys()) - set(res_id_current))
[res_all.pop(key) for key in res_id_served]
res_id_picked = [res.id for res in traci.person.getTaxiReservations(8)]
if options.verbose:
print('Solve DARP with %s' % options.darp_solver)
darp_solution = darpSolvers.main(options, step, fleet, veh_type,
veh_time_pickup, veh_time_dropoff,
res_all, res_id_new,
res_id_unassigned, res_id_picked,
veh_edges,
pairs_dua_times)
routes, ilp_res_cons, exact_sol = darp_solution
if len(routes) == 0:
step += options.sim_step
continue
elif ilp_res_cons is None:
best_routes = list(routes.keys())
else:
veh_constraints = {}
res_constraints = {}
costs = {}
trips = list(sorted(routes.keys()))
for idx, trip_id in enumerate(trips):
bonus_cost = (sum(routes[trip_id][2]) + 1) * options.cost_per_trip
costs.update({idx: routes[trip_id][0] + bonus_cost})
veh_constraints.update({idx: routes[trip_id][1]})
res_constraints.update({idx: routes[trip_id][2]})
if options.verbose:
print('Solve ILP')
ilp_result = ilp_solve(options, len(fleet), len(ilp_res_cons),
costs, veh_constraints, res_constraints)
best_routes = [trips[int(route_index)]
for route_index in ilp_result]
for route_id in sorted(best_routes):
stops = route_id.replace('y', '').replace('z', '').split("_")
veh_id = stops[0]
current_route = []
if traci.vehicle.getStops(veh_id):
for taxi_stop in traci.vehicle.getStops(veh_id):
next_act = taxi_stop.actType.split(",")[0].split(" ")[0]
if not next_act:
continue
next_id = taxi_stop.actType.split(",")[0].split(" ")[-1][1:-1]
if next_act == 'pickup' and next_id in res_id_picked:
continue
elif next_act == 'dropOff' and next_id not in res_all.keys():
continue
sub_stops = taxi_stop.actType.split(",")
for sub_stop in sub_stops:
current_route.append(sub_stop.split(" ")[2][1:-1])
if current_route == stops[1:]:
continue
elif (set(current_route) == set(stops[1:]) and
len(current_route) == len(stops[1:])):
tt_current_route = step
edges = [taxi_stop.lane.split("_")[0] for taxi_stop
in traci.vehicle.getStops(veh_id)]
stop_types = [taxi_stop.actType for taxi_stop
in traci.vehicle.getStops(veh_id)]
if traci.vehicle.getRoadID(veh_id).startswith(':'):
edge_index = traci.vehicle.getRouteIndex(veh_id) + 1
veh_edge = traci.vehicle.getRoute(veh_id)[edge_index]
else:
veh_edge = traci.vehicle.getRoadID(veh_id)
edges.insert(0, veh_edge)
for idx, edge in enumerate(edges[:-1]):
tt_pair = pairs_dua_times.get("%s_%s" % (edge,
edges[idx+1]))
if tt_pair is None:
tt_pair = int(findRoute(edge, edges[idx+1],
veh_type, step, routingMode=options.routing_mode).travelTime)
if 'pickup' in stop_types[idx]:
tt_current_route += tt_pair + veh_time_pickup
else:
tt_current_route += tt_pair + veh_time_dropoff
tt_new_route = routes[route_id][0]
if tt_new_route >= tt_current_route:
continue
if options.verbose:
print('Dispatch:', route_id)
traci.vehicle.dispatchTaxi(veh_id, stops[1:])
for res_id in set(stops[1:]):
res = res_all[res_id]
res.vehicle = veh_id
if step > options.end_time:
rerouting = False
step += options.sim_step
if all(exact_sol):
print('\nExact solution found.')
else:
print('\nApproximate solution found.')
print('DRT simulation ended')
traci.close()
if __name__ == "__main__":
main()