Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/drt/drtOrtools.py
169673 views
1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
4
# Copyright (C) 2021-2025 German Aerospace Center (DLR) and others.
5
# This program and the accompanying materials are made available under the
6
# terms of the Eclipse Public License 2.0 which is available at
7
# https://www.eclipse.org/legal/epl-2.0/
8
# This Source Code may also be made available under the following Secondary
9
# Licenses when the conditions for such availability set forth in the Eclipse
10
# Public License 2.0 are satisfied: GNU General Public License, version 2
11
# or later which is available at
12
# https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
13
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
14
15
# @file drtOrtools.py
16
# @author Philip Ritzer
17
# @author Johannes Rummel
18
# @author Mirko Barthauer
19
# @date 2021-12-16
20
21
"""
22
Prototype online DRT algorithm using ortools via TraCI.
23
"""
24
# needed for type alias in python < 3.9
25
from __future__ import annotations
26
from typing import List, Dict, Tuple
27
28
import os
29
import sys
30
import argparse
31
32
import ortools_pdp
33
import orToolsDataModel
34
35
# we need to import python modules from the $SUMO_HOME/tools directory
36
if 'SUMO_HOME' in os.environ:
37
sys.path.append(os.path.join(os.environ['SUMO_HOME'], 'tools'))
38
39
# SUMO modules
40
import sumolib # noqa
41
import traci # noqa
42
43
PENALTY_FACTOR = 'dynamic' # factor on penalty for rejecting requests
44
45
46
# use 'type' statement in python version 3.12 or higher
47
RequestOrder = List[str]
48
Cost = int
49
NodeOrder = List[int]
50
TranslatedSolutions = Dict[int, Tuple[RequestOrder, Cost, NodeOrder]]
51
52
# TODO: solution_requests is not needed as input
53
54
55
def dispatch(time_limit: int, solution_requests: TranslatedSolutions,
56
data: orToolsDataModel.ORToolsDataModel, verbose: bool) -> TranslatedSolutions | None:
57
"""Dispatch using ortools."""
58
if verbose:
59
print('Start solving the problem.')
60
solution_ortools = ortools_pdp.main(data, time_limit, verbose)
61
if verbose:
62
print('Start interpreting the solution for SUMO.')
63
solution_requests = solution_by_requests(solution_ortools, data, verbose)
64
return solution_requests
65
66
67
def create_data_model(sumo_fleet: list[str], cost_type: orToolsDataModel.CostType,
68
drf: float, waiting_time: int, end: int,
69
fix_allocation: bool, solution_requests: TranslatedSolutions | None, penalty_factor: str | int,
70
data_reservations: list[orToolsDataModel.Reservation],
71
timestep: float, verbose: bool) -> orToolsDataModel.ORToolsDataModel:
72
"""Creates the data for the problem."""
73
vehicles = orToolsDataModel.create_vehicles(sumo_fleet)
74
updated_reservations = orToolsDataModel.update_reservations(data_reservations)
75
new_reservations = orToolsDataModel.create_new_reservations(updated_reservations)
76
reservations = new_reservations + updated_reservations
77
reservations, rejected_reservations = orToolsDataModel.reject_late_reservations(
78
reservations, waiting_time, timestep)
79
orToolsDataModel.map_vehicles_to_reservations(vehicles, reservations)
80
node_objects = orToolsDataModel.create_nodes(reservations, vehicles)
81
82
n_vehicles = len(vehicles)
83
if verbose:
84
if rejected_reservations:
85
print(f"Reservations rejected: {[res.get_id() for res in rejected_reservations]}")
86
print(f'dp reservations: {[res.get_id() for res in reservations if not res.is_picked_up()]}')
87
print(f'do reservations: {[res.get_id() for res in reservations if res.is_picked_up()]}')
88
for reservation in reservations:
89
if not reservation.is_picked_up():
90
print(f'Reservation {reservation.get_id()} starts at edge {reservation.get_from_edge()}')
91
for reservation in reservations:
92
if not reservation.is_picked_up():
93
print(f'Reservation {reservation.get_id()} ends at edge {reservation.get_to_edge()}')
94
for reservation in reservations:
95
if reservation.is_picked_up():
96
print(f'Drop-off of reservation {reservation.get_id()} at edge {reservation.get_to_edge()}')
97
98
vehicle_capacities = [veh.get_person_capacity() for veh in vehicles]
99
100
types_vehicle = [veh.get_type_ID() for veh in vehicles]
101
types_vehicles_unique = list(set(types_vehicle))
102
if len(types_vehicles_unique) > 1:
103
raise Exception("Only one vehicle type is supported.")
104
# TODO support more than one vehicle type
105
else:
106
type_vehicle = types_vehicles_unique[0]
107
cost_matrix, time_matrix = orToolsDataModel.get_cost_matrix(node_objects, cost_type)
108
109
# safe cost and time matrix
110
if verbose:
111
import csv
112
with open("cost_matrix.csv", 'a') as cost_file:
113
wr = csv.writer(cost_file)
114
wr.writerows(cost_matrix)
115
with open("time_matrix.csv", 'a') as time_file:
116
wr = csv.writer(time_file)
117
wr.writerows(time_matrix)
118
119
# add "direct route cost" to the requests:
120
for reservation in reservations:
121
reservation.update_direct_route_cost(type_vehicle, cost_matrix, cost_type)
122
if verbose:
123
print(f'Reservation {reservation.get_id()} has direct route costs {reservation.direct_route_cost}')
124
125
# add "current route cost" to the already picked up reservations:
126
for reservation in reservations:
127
if reservation.is_picked_up():
128
reservation.update_current_route_cost(cost_type)
129
130
start_nodes = [veh.start_node for veh in vehicles]
131
132
demands = [orToolsDataModel.get_demand_of_node_object(node_object, node)
133
for node, node_object in enumerate(node_objects)]
134
135
# get time windows
136
time_windows = [orToolsDataModel.get_time_window_of_node_object(node_object, node, end)
137
for node, node_object in enumerate(node_objects)]
138
139
penalty = orToolsDataModel.get_penalty(penalty_factor, cost_matrix)
140
if verbose:
141
print(f'Penalty factor is {penalty}')
142
143
data = orToolsDataModel.ORToolsDataModel(
144
depot=0,
145
cost_matrix=cost_matrix,
146
time_matrix=time_matrix,
147
pickups_deliveries=[res for res in reservations if not res.is_picked_up()], # dp_reservations
148
dropoffs=[res for res in reservations if res.is_picked_up()], # do_reservations
149
num_vehicles=n_vehicles,
150
starts=start_nodes,
151
ends=n_vehicles * [0], # end at 'depot', which is is anywere
152
demands=demands, # [0] + n_dp_reservations*[1] + n_dp_reservations*[-1] + n_do_reservations*[-1] + veh_demand
153
vehicle_capacities=vehicle_capacities,
154
drf=drf,
155
waiting_time=waiting_time,
156
time_windows=time_windows,
157
fix_allocation=fix_allocation,
158
max_time=end,
159
initial_routes=solution_requests,
160
penalty=penalty,
161
reservations=reservations,
162
vehicles=vehicles,
163
cost_type=cost_type
164
)
165
return data
166
167
168
def get_max_time() -> int:
169
max_sim_time = traci.simulation.getEndTime()
170
if max_sim_time == -1:
171
return 90000
172
else:
173
return max_sim_time
174
175
176
def solution_by_requests(solution_ortools: ortools_pdp.ORToolsSolution | None,
177
data: orToolsDataModel.ORToolsDataModel, verbose: bool = False) -> TranslatedSolutions | None:
178
"""Translate solution from ortools to SUMO requests."""
179
if solution_ortools is None:
180
return None
181
182
# dp_reservations = [res for res in reservations if res.state != 8]
183
184
route2request = {}
185
for res in data.pickups_deliveries:
186
route2request[res.from_node] = res.get_id()
187
route2request[res.to_node] = res.get_id()
188
for res in data.dropoffs: # for each vehicle
189
route2request[res.to_node] = res.get_id()
190
191
solution_requests: TranslatedSolutions = {}
192
for key in solution_ortools: # key is the vehicle number (0,1,...)
193
request_order: RequestOrder = []
194
node_order: NodeOrder = []
195
for i_route in solution_ortools[key][0][1:-1]: # take only the routes ([0]) without the start node ([1:-1])
196
if i_route in route2request:
197
request_order.append(route2request[i_route]) # add request id to route
198
# [res for res in data["pickups_deliveries"]+data['dropoffs']
199
# if res.get_id() == route2request[i_route]][0] # get the reservation
200
res = orToolsDataModel.get_reservation_by_node(data.pickups_deliveries+data.dropoffs, i_route)
201
res.vehicle = orToolsDataModel.get_vehicle_by_vehicle_index(data.vehicles, key)
202
else:
203
if verbose:
204
print(f'!solution ignored: {i_route}')
205
continue
206
node_order.append(i_route) # node
207
cost: Cost = solution_ortools[key][1] # cost
208
solution_requests[key] = (request_order, cost, node_order)
209
return solution_requests
210
211
212
def run(penalty_factor: str | int, end: int = None, interval: int = 30, time_limit: float = 10,
213
cost_type: orToolsDataModel.CostType = orToolsDataModel.CostType.DISTANCE,
214
drf: float = 1.5, waiting_time: int = 900, fix_allocation: bool = False, verbose: bool = False):
215
"""
216
Execute the TraCI control loop and run the scenario.
217
218
Parameters
219
----------
220
penalty_factor: 'dynamic' | int
221
Penalty for rejecting requests or exceeding drf, time windows and waiting times.
222
If 'dynamic' then the factor is set depending on the maximum cost.
223
end : int, optional
224
Final time step of simulation. The default is 90000.
225
This option can be ignored by giving a negative value.
226
interval : int, optional
227
Dispatching interval in s. The default is 30.
228
time_limit: float, optional
229
Time limit for solver in s. The default is 10.
230
cost_type: str, optional
231
Type of costs. The default is 'distance'. Another option is 'time'.
232
verbose : bool, optional
233
Controls whether debug information is printed. The default is True.
234
"""
235
running = True
236
timestep = traci.simulation.getTime()
237
if not end:
238
end = get_max_time()
239
240
if verbose:
241
print('Simulation parameters:')
242
print(f' end: {end}')
243
print(f' interval: {interval}')
244
print(f' time_limit: {time_limit}')
245
print(f' cost_type: {cost_type}')
246
print(f' drf: {drf}')
247
print(f' waiting_time: {waiting_time}')
248
print(f' fix_allocation: {fix_allocation}')
249
250
solution_requests = None
251
data_reservations = list()
252
while running:
253
254
traci.simulationStep(timestep)
255
256
# termination condition
257
if timestep > end:
258
running = False
259
continue
260
261
if not traci.vehicle.getTaxiFleet(-1) and timestep < end:
262
timestep += interval
263
continue
264
265
if verbose:
266
print(f"timestep: {timestep}")
267
res_waiting = [res.id for res in traci.person.getTaxiReservations(3)]
268
res_pickup = [res.id for res in traci.person.getTaxiReservations(4)]
269
res_transport = [res.id for res in traci.person.getTaxiReservations(8)]
270
if res_waiting:
271
print(f"Reservations waiting: {res_waiting}")
272
if res_pickup:
273
print(f"Reservations being picked up: {res_pickup}")
274
if res_transport:
275
print(f"Reservations en route: {res_transport}")
276
fleet_empty = traci.vehicle.getTaxiFleet(0)
277
fleet_pickup = traci.vehicle.getTaxiFleet(1)
278
fleet_occupied = traci.vehicle.getTaxiFleet(2)
279
fleet_occupied_pickup = traci.vehicle.getTaxiFleet(3)
280
if fleet_empty:
281
print(f"Taxis empty: {fleet_empty}")
282
if fleet_pickup:
283
print(f"Taxis picking up: {fleet_pickup}")
284
if fleet_occupied:
285
print(f"Taxis occupied: {fleet_occupied}")
286
if fleet_occupied_pickup:
287
print(f"Taxis occupied and picking up: {fleet_occupied_pickup}")
288
289
fleet = traci.vehicle.getTaxiFleet(-1)
290
# take reservations, that are not assigned to a taxi (state 1: new + state 2: already retrieved)
291
reservations_not_assigned = traci.person.getTaxiReservations(3)
292
293
# if reservations_all: # used for debugging
294
if reservations_not_assigned:
295
if verbose:
296
print("Solve CPDP")
297
if verbose:
298
print('Start creating the model.')
299
data = create_data_model(fleet, cost_type, drf, waiting_time, int(end),
300
fix_allocation, solution_requests, penalty_factor,
301
data_reservations, timestep, verbose)
302
data_reservations = data.reservations
303
solution_requests = dispatch(time_limit, solution_requests, data, verbose)
304
if solution_requests is not None:
305
for index_vehicle, vehicle_requests in solution_requests.items(): # for each vehicle
306
id_vehicle = fleet[index_vehicle]
307
reservations_order = [res_id for res_id in vehicle_requests[0]] # [0] for route
308
if verbose:
309
print(f"Dispatching {id_vehicle} with {reservations_order}")
310
print(f"Costs for {id_vehicle}: {vehicle_requests[1]}")
311
if fix_allocation and not reservations_order: # ignore empty reservations if allocation is fixed
312
continue
313
traci.vehicle.dispatchTaxi(id_vehicle, reservations_order) # overwrite existing dispatch
314
else:
315
if verbose:
316
print("Found no solution, continue...")
317
318
timestep += interval
319
320
# Finish
321
traci.close()
322
sys.stdout.flush()
323
324
325
def dynamic_or_int(value):
326
if value == 'dynamic':
327
return value
328
try:
329
return int(value)
330
except ValueError:
331
raise ValueError(f"Wrong value for penalty factor '{value}'. Must be 'dynamic' or an integer.")
332
333
334
def get_arguments() -> argparse.Namespace:
335
"""Get command line arguments."""
336
ap = sumolib.options.ArgumentParser()
337
ap.add_argument("-s", "--sumo-config", required=True, category="input", type=ap.file,
338
help="sumo config file to run")
339
ap.add_argument("-e", "--end", type=ap.time,
340
help="time step to end simulation at")
341
ap.add_argument("-i", "--interval", type=ap.time, default=30,
342
help="dispatching interval in s")
343
ap.add_argument("-n", "--nogui", action="store_true", default=False,
344
help="run the commandline version of sumo")
345
ap.add_argument("-v", "--verbose", action="store_true", default=False,
346
help="print debug information")
347
ap.add_argument("-t", "--time-limit", type=ap.time, default=10,
348
help="time limit for solver in s")
349
ap.add_argument("-d", "--cost-type", default="distance",
350
help="type of costs to minimize (distance or time)")
351
ap.add_argument("-f", "--drf", type=float, default=1.5,
352
help="direct route factor to calculate maximum cost "
353
"for a single dropoff-pickup route (set to -1, if you do not need it)")
354
ap.add_argument("-a", "--fix-allocation", action="store_true", default=False,
355
help="if true: after first solution the allocation of reservations to vehicles" +
356
"does not change anymore")
357
ap.add_argument("-w", "--waiting-time", type=ap.time, default=900,
358
help="maximum waiting time to serve a request in s")
359
ap.add_argument("-p", "--penalty-factor", type=dynamic_or_int, default=PENALTY_FACTOR,
360
help="factor on penalty for rejecting requests, must be 'dynamic' or an integer (e.g. 100000)")
361
ap.add_argument("--trace-file", type=ap.file,
362
help="log file for TraCI debugging")
363
return ap.parse_args()
364
365
366
def check_set_arguments(arguments: argparse.Namespace):
367
if arguments.nogui:
368
arguments.sumoBinary = sumolib.checkBinary('sumo')
369
else:
370
arguments.sumoBinary = sumolib.checkBinary('sumo-gui')
371
372
# set cost type
373
if arguments.cost_type == "distance":
374
arguments.cost_type = orToolsDataModel.CostType.DISTANCE
375
elif arguments.cost_type == "time":
376
arguments.cost_type = orToolsDataModel.CostType.TIME
377
else:
378
raise ValueError(f"Wrong cost type '{arguments.cost_type}'. Only 'distance' and 'time' are allowed.")
379
380
if arguments.drf < 1 and arguments.drf != -1:
381
raise ValueError(
382
f"Wrong value for drf '{arguments.drf}'. Value must be equal or greater than 1. -1 means no drf is used.")
383
384
if arguments.waiting_time < 0:
385
raise ValueError(
386
f"Wrong value for waiting time '{arguments.waiting_time}'. Value must be equal or greater than 0.")
387
388
389
if __name__ == "__main__":
390
391
arguments = get_arguments()
392
check_set_arguments(arguments)
393
# this script has been called from the command line. It will start sumo as a
394
# server, then connect and run
395
396
# this is the normal way of using traci. sumo is started as a
397
# subprocess and then the python script connects and runs
398
traci.start([arguments.sumoBinary, "-c", arguments.sumo_config], traceFile=arguments.trace_file)
399
400
run(arguments.penalty_factor, arguments.end, arguments.interval,
401
arguments.time_limit, arguments.cost_type, arguments.drf,
402
arguments.waiting_time, arguments.fix_allocation, arguments.verbose)
403
404