Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/drt/orToolsDataModel.py
169673 views
1
# -*- coding: utf-8 -*-
2
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3
# Copyright (C) 2021-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 orToolsDataModel.py
15
# @author Johannes Rummel
16
# @date 2024-03-13
17
18
"""
19
Data model for drtOrtools.py to solve a drt problem with the ortools routing solver.
20
"""
21
# needed for type alias in python < 3.9
22
from __future__ import annotations
23
import os
24
import sys
25
import typing
26
from dataclasses import dataclass
27
from enum import Enum
28
import numpy as np
29
import math
30
31
# SUMO modules
32
# we need to import python modules from the $SUMO_HOME/tools directory
33
if 'SUMO_HOME' in os.environ:
34
sys.path.append(os.path.join(os.environ['SUMO_HOME'], 'tools'))
35
import traci # noqa
36
import traci._person
37
import traci._simulation
38
39
SPEED_DEFAULT = 20 # default vehicle speed in m/s
40
41
42
class CostType(Enum):
43
DISTANCE = 1
44
TIME = 2
45
46
47
@dataclass
48
class Vehicle:
49
"""
50
Represents a vehicle/route for the routing problem.
51
"""
52
id_vehicle: str # vehicle id/name from SUMO
53
vehicle_index: int
54
start_node: int = None
55
end_node: int = None
56
57
def get_person_capacity(self) -> int:
58
return traci.vehicle.getPersonCapacity(self.id_vehicle)
59
60
def get_type_ID(self) -> str:
61
return traci.vehicle.getTypeID(self.id_vehicle)
62
63
def get_edge(self) -> str:
64
return traci.vehicle.getRoadID(self.id_vehicle)
65
66
def get_person_id_list(self) -> list[str]:
67
return traci.vehicle.getPersonIDList(self.id_vehicle)
68
69
70
@dataclass
71
class Reservation:
72
"""
73
Represents a request for a transportation.
74
"""
75
reservation: traci._person.Reservation
76
from_node: int = None
77
to_node: int = None
78
direct_route_cost: int = None
79
current_route_cost: int = None
80
vehicle: Vehicle = None
81
82
def is_new(self) -> bool:
83
if self.reservation.state == 1 or self.reservation.state == 2:
84
return True
85
else:
86
return False
87
88
def is_picked_up(self) -> bool:
89
return self.reservation.state == 8
90
91
def is_from_node(self, node: int) -> bool:
92
return (not self.is_picked_up() and self.from_node == node)
93
94
def is_to_node(self, node: int) -> bool:
95
return self.to_node == node
96
97
def get_from_edge(self) -> str:
98
return self.reservation.fromEdge
99
100
def get_to_edge(self) -> str:
101
return self.reservation.toEdge
102
103
def get_id(self) -> str:
104
return self.reservation.id
105
106
def get_persons(self) -> list[str]:
107
return self.reservation.persons
108
109
def get_earliest_pickup(self) -> int:
110
person_id = self.get_persons()[0]
111
pickup_earliest = (traci.person.getParameter(person_id, "pickup_earliest")
112
or traci.person.getParameter(person_id, "earliestPickupTime"))
113
if pickup_earliest:
114
pickup_earliest = round(float(pickup_earliest))
115
return pickup_earliest
116
117
def get_dropoff_latest(self) -> int:
118
person_id = self.get_persons()[0]
119
dropoff_latest = (traci.person.getParameter(person_id, "dropoff_latest")
120
or traci.person.getParameter(person_id, "latestDropoffTime"))
121
if dropoff_latest:
122
dropoff_latest = round(float(dropoff_latest))
123
return dropoff_latest
124
125
def update_direct_route_cost(self, type_vehicle: str, cost_matrix: list[list[int]] = None,
126
cost_type: CostType = CostType.DISTANCE):
127
if self.direct_route_cost:
128
return
129
if not self.is_picked_up():
130
self.direct_route_cost = cost_matrix[self.from_node][self.to_node]
131
else:
132
# TODO: use 'historical data' from dict in get_cost_matrix instead
133
route: traci._simulation.Stage = traci.simulation.findRoute(
134
self.get_from_edge(), self.get_to_edge(), vType=type_vehicle)
135
if cost_type == CostType.TIME:
136
self.direct_route_cost = round(route.travelTime)
137
elif cost_type == CostType.DISTANCE:
138
self.direct_route_cost = round(route.length)
139
else:
140
raise ValueError(f"Cannot set given cost ({cost_type}).")
141
142
def update_current_route_cost(self, cost_type: CostType = CostType.DISTANCE):
143
person_id = self.reservation.persons[0]
144
stage: traci._simulation.Stage = traci.person.getStage(person_id, 0)
145
# stage type '3' is defined as 'driving'
146
assert stage.type == 3
147
if cost_type == CostType.DISTANCE:
148
self.current_route_cost = round(stage.length)
149
elif cost_type == CostType.TIME:
150
self.current_route_cost = round(stage.travelTime)
151
else:
152
raise ValueError(f"Cannot set given cost ({cost_type}).")
153
154
155
@dataclass
156
class ORToolsDataModel:
157
"""
158
Data model class used by constrains of the OR-tools lib.
159
"""
160
# nodeID of the depot
161
depot: int
162
cost_matrix: list[list[int]]
163
time_matrix: list[list[int]]
164
pickups_deliveries: list[Reservation]
165
dropoffs: list[Reservation]
166
num_vehicles: int
167
starts: list[int]
168
ends: list[int]
169
demands: list[int]
170
vehicle_capacities: list[int]
171
drf: float
172
waiting_time: int
173
time_windows: list[(int, int)]
174
fix_allocation: bool
175
max_time: int
176
initial_routes: dict[int: list[list[int]]]
177
penalty: int
178
reservations: list[Reservation]
179
vehicles: list[Vehicle]
180
cost_type: CostType
181
182
def __str__(self):
183
return f'number of vehicles: {self.num_vehicles}, ...'
184
185
def get_penalty(self, explicitly_time_related: bool = False) -> int:
186
"""Returns penalty. If explicitly time related, it depends on the CostType of the data."""
187
if not explicitly_time_related:
188
return self.penalty
189
if self.cost_type == CostType.DISTANCE:
190
return round(self.penalty * SPEED_DEFAULT)
191
else:
192
return self.penalty
193
194
195
@dataclass
196
class Node:
197
"""
198
Connects an object of the routing problem with a nodeID.
199
"""
200
class NodeType(Enum):
201
FROM_EDGE = 1
202
TO_EDGE = 2
203
VEHICLE = 3
204
DEPOT = 4
205
206
# id: int = field(default_factory=...)
207
node_type: NodeType
208
209
210
# use 'type' statement in python version 3.12 or higher
211
NodeObject = typing.Union[str, Vehicle, Reservation]
212
213
214
def create_nodes(reservations: list[Reservation], vehicles: list[Vehicle]) -> list[NodeObject]:
215
"""
216
Sets the node ids from 0...n for the locations of the start and
217
end points of the reservations and vehicles.
218
"""
219
n = 0 # reserved for depot (which can also be a free location)
220
node_objects = ['depot']
221
n += 1
222
for res in reservations:
223
if not res.is_picked_up():
224
node_objects.append(res)
225
res.from_node = n
226
n += 1
227
node_objects.append(res)
228
res.to_node = n
229
n += 1
230
for veh in vehicles:
231
node_objects.append(veh)
232
veh.start_node = n
233
n += 1
234
veh.end_node = 0 # currently all vehicles end at depot
235
# TODO: to generalize the end nodes, separate nodes are needed
236
return node_objects
237
238
239
def create_vehicles(fleet: list[str]) -> list[Vehicle]:
240
vehicles = []
241
for i, veh_id in enumerate(fleet):
242
veh = Vehicle(veh_id, i)
243
vehicles.append(veh)
244
return vehicles
245
246
247
# list[traci.Person.Reservation]
248
def create_new_reservations(data_reservations: list[Reservation]) -> list[Reservation]:
249
"""create Reservations that not already exist"""
250
sumo_reservations = traci.person.getTaxiReservations(0) # TODO: state 1 should be enough
251
252
data_reservations_ids = [res.get_id() for res in data_reservations]
253
new_reservations = []
254
for res in sumo_reservations:
255
if res.id not in data_reservations_ids:
256
new_reservations.append(Reservation(res))
257
return new_reservations
258
259
260
def update_reservations(data_reservations: list[Reservation]) -> list[Reservation]:
261
"""update the Reservation.reservation and also remove Reservations that are completed"""
262
sumo_reservations: tuple[traci._person.Reservation] = traci.person.getTaxiReservations(0)
263
updated_reservations = []
264
for data_reservation in data_reservations:
265
new_res = [res for res in sumo_reservations if res.id == data_reservation.get_id()]
266
if new_res:
267
data_reservation.reservation = new_res[0]
268
updated_reservations.append(data_reservation)
269
return updated_reservations
270
271
272
def reject_late_reservations(data_reservations: list[Reservation], waiting_time: int,
273
timestep: float) -> tuple[list[Reservation], list[Reservation]]:
274
"""
275
rejects reservations that are not assigned to a vehicle and cannot be served by time
276
277
Returns a cleared list and a list of the removed reservations.
278
"""
279
new_data_reservations = []
280
rejected_reservations = []
281
for data_reservation in data_reservations:
282
if not data_reservation.vehicle and data_reservation.reservation.reservationTime + waiting_time < timestep:
283
for person in data_reservation.get_persons():
284
traci.person.removeStages(person)
285
rejected_reservations.append(data_reservation)
286
else:
287
new_data_reservations.append(data_reservation)
288
return new_data_reservations, rejected_reservations
289
290
291
def map_vehicles_to_reservations(vehicles: list[Vehicle], reservations: list[Reservation]) -> None:
292
"""
293
Sets the vehicle attribute of the reservations with the vehicle that contains the same persons.
294
"""
295
for vehicle in vehicles:
296
persons_in_vehicle = vehicle.get_person_id_list()
297
for reservation in reservations:
298
if reservation.get_persons()[0] in persons_in_vehicle:
299
reservation.vehicle = vehicle
300
301
302
def get_edge_of_node_object(node_object: NodeObject, node: int) -> str | None:
303
"""
304
Returns the edge of the given NodeObject. "node" is needed for Reservations,
305
to make clear if the edge of the departure or destination is searched.
306
Returns "None" if an edge cannot be found.
307
"""
308
if isinstance(node_object, Vehicle):
309
return node_object.get_edge()
310
if isinstance(node_object, Reservation):
311
if node_object.is_from_node(node):
312
return node_object.get_from_edge()
313
if node_object.is_to_node(node):
314
return node_object.get_to_edge()
315
return None
316
317
318
def get_demand_of_node_object(node_object: NodeObject, node: int) -> int | None:
319
"""
320
Returns "None" if node is not from_node or to_node of a reservation.
321
"""
322
if isinstance(node_object, str) and node_object == 'depot':
323
return 0
324
if isinstance(node_object, Vehicle):
325
return traci.vehicle.getPersonNumber(node_object.id_vehicle)
326
if isinstance(node_object, Reservation):
327
if node_object.is_from_node(node):
328
return 1
329
if node_object.is_to_node(node):
330
return -1
331
return None
332
333
334
# TODO: If cost_type is TIME, remove cost_matrix and cost_dict.
335
def get_cost_matrix(node_objects: list[NodeObject], cost_type: CostType):
336
"""Get cost matrix between edges.
337
Index in cost matrix is the same as the node index of the constraint solver."""
338
339
# get vehicle type of one vehicle (we suppose all vehicles are of the same type)
340
type_vehicle, id_vehicle = next(((x.get_type_ID(), x.id_vehicle)
341
for x in node_objects if isinstance(x, Vehicle)), None)
342
boardingDuration_param = traci.vehicletype.getBoardingDuration(type_vehicle)
343
boardingDuration = 0 if boardingDuration_param == '' else round(float(boardingDuration_param))
344
# TODO: pickup and dropoff duration of first vehicle is used for all vehicles!!!
345
pickUpDuration_param = traci.vehicle.getParameter(id_vehicle, 'device.taxi.pickUpDuration')
346
pickUpDuration = 0 if pickUpDuration_param == '' else round(float(pickUpDuration_param))
347
dropOffDuration_param = traci.vehicle.getParameter(id_vehicle, 'device.taxi.dropOffDuration')
348
dropOffDuration = 0 if dropOffDuration_param == '' else round(float(dropOffDuration_param))
349
n_edges = len(node_objects)
350
time_matrix = np.zeros([n_edges, n_edges], dtype=int)
351
cost_matrix = np.zeros([n_edges, n_edges], dtype=int)
352
time_dict = {}
353
cost_dict = {}
354
# TODO initialize cost_dict and time_dict{} in run() and update for speed improvement
355
for ii, from_node_object in enumerate(node_objects):
356
edge_from = get_edge_of_node_object(from_node_object, ii)
357
for jj, to_node_object in enumerate(node_objects):
358
edge_to = get_edge_of_node_object(to_node_object, jj)
359
# cost to depot should be always 0
360
# (means there is no way to depot in the end)
361
if from_node_object == 'depot' or to_node_object == 'depot':
362
time_matrix[ii][jj] = 0
363
cost_matrix[ii][jj] = 0
364
continue
365
if ii == jj:
366
time_matrix[ii][jj] = 0
367
cost_matrix[ii][jj] = 0
368
continue
369
if (edge_from, edge_to) in cost_dict:
370
# get costs from previous call
371
time_matrix[ii][jj] = time_dict[(edge_from, edge_to)]
372
cost_matrix[ii][jj] = cost_dict[(edge_from, edge_to)]
373
continue
374
# TODO: findRoute is not needed between two vehicles
375
route: traci._simulation.Stage = traci.simulation.findRoute(edge_from, edge_to, vType=type_vehicle)
376
time_matrix[ii][jj] = round(route.travelTime)
377
if isinstance(from_node_object, Reservation) and from_node_object.is_from_node(ii):
378
time_matrix[ii][jj] += pickUpDuration # add pickup_duration
379
time_matrix[ii][jj] += boardingDuration # add boarding_duration
380
if isinstance(to_node_object, Reservation) and to_node_object.is_to_node(jj):
381
time_matrix[ii][jj] += dropOffDuration # add dropoff_duration
382
time_dict[(edge_from, edge_to)] = time_matrix[ii][jj]
383
if cost_type == CostType.TIME:
384
cost_matrix[ii][jj] = time_matrix[ii][jj]
385
cost_dict[(edge_from, edge_to)] = time_dict[(edge_from, edge_to)]
386
elif cost_type == CostType.DISTANCE:
387
cost_matrix[ii][jj] = round(route.length)
388
cost_dict[(edge_from, edge_to)] = round(route.length)
389
return cost_matrix.tolist(), time_matrix.tolist()
390
391
392
def get_time_window_of_node_object(node_object: NodeObject, node: int, end: int) -> tuple[int, int]:
393
"""returns a pair with earliest and latest service time"""
394
current_time = round(traci.simulation.getTime())
395
max_time = round(end)
396
397
time_window = None
398
if isinstance(node_object, str) and node_object == 'depot':
399
time_window = (current_time, max_time)
400
elif isinstance(node_object, Vehicle):
401
# TODO: throws an exception if not set: traci.vehicle.getParameter(node_object.id_vehicle, 'device.taxi.end')
402
device_taxi_end = max_time
403
time_window_end = max_time if device_taxi_end == '' else round(float(device_taxi_end))
404
time_window = (current_time, time_window_end)
405
elif isinstance(node_object, Reservation):
406
if node_object.is_from_node(node):
407
pickup_earliest = node_object.get_earliest_pickup() or current_time
408
time_window = (pickup_earliest, max_time)
409
if node_object.is_to_node(node):
410
dropoff_latest = node_object.get_dropoff_latest() or max_time
411
time_window = (current_time, dropoff_latest)
412
else:
413
raise ValueError(f"Cannot set time window for node {node}.")
414
return time_window
415
416
417
def get_vehicle_by_vehicle_index(vehicles: list[Vehicle], index: int) -> Vehicle:
418
for vehicle in vehicles:
419
if vehicle.vehicle_index == index:
420
return vehicle
421
return None
422
423
424
def get_reservation_by_node(reservations: list[Reservation], node: int) -> Reservation:
425
for reservation in reservations:
426
if reservation.is_from_node(node) or reservation.is_to_node(node):
427
return reservation
428
return None
429
430
431
def get_penalty(penalty_factor: str | int, cost_matrix: list[list[int]]) -> int:
432
if penalty_factor == 'dynamic':
433
max_cost = max(max(sublist) for sublist in cost_matrix)
434
return round_up_to_next_power_of_10(max_cost)
435
else:
436
return penalty_factor
437
438
439
def round_up_to_next_power_of_10(n: int) -> int:
440
if n < 0:
441
raise ValueError(f"Input '{n}' must be a positive integer")
442
if n == 0:
443
return 1
444
# Determine the number of digits of the input value
445
num_digits = math.floor(math.log10(n)) + 1
446
scale = 10 ** (num_digits - 1)
447
leading_digit = n // scale
448
# If the input value is not already a power of 10, increase the leading digit by 1
449
if n % scale != 0:
450
leading_digit += 1
451
rounded_value = leading_digit * scale
452
return rounded_value
453
454