eflips-depot 4.3.17__py3-none-any.whl → 4.3.18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eflips/depot/api/__init__.py +19 -979
- eflips/depot/api/private/depot.py +360 -1
- eflips/depot/api/private/results_to_database.py +586 -0
- {eflips_depot-4.3.17.dist-info → eflips_depot-4.3.18.dist-info}/METADATA +2 -1
- {eflips_depot-4.3.17.dist-info → eflips_depot-4.3.18.dist-info}/RECORD +7 -6
- {eflips_depot-4.3.17.dist-info → eflips_depot-4.3.18.dist-info}/WHEEL +1 -1
- {eflips_depot-4.3.17.dist-info → eflips_depot-4.3.18.dist-info}/LICENSE.md +0 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import itertools
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
from typing import List, Dict
|
|
6
|
+
|
|
7
|
+
from eflips.model import Event, EventType, Rotation, Vehicle, Area
|
|
8
|
+
from sqlalchemy import select
|
|
9
|
+
|
|
10
|
+
from eflips.depot import SimpleVehicle, ProcessStatus
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_finished_schedules_per_vehicle(
|
|
14
|
+
dict_of_events, list_of_finished_trips: List, db_vehicle_id: int
|
|
15
|
+
):
|
|
16
|
+
"""
|
|
17
|
+
This function completes the following tasks:
|
|
18
|
+
|
|
19
|
+
1. It gets the finished non-copy schedules of the current vehicle,
|
|
20
|
+
which will be used in :func:`_update_vehicle_in_rotation()`.
|
|
21
|
+
|
|
22
|
+
2. It fills the dictionary of events with the trip_ids of the current vehicle.
|
|
23
|
+
|
|
24
|
+
3. It returns an earliest and a latest time according to this vehicle's schedules. Only processes happening within
|
|
25
|
+
this time window will be handled later.
|
|
26
|
+
|
|
27
|
+
Usually the earliest time is the departure time of the last copy trip in the "early-shifted" copy schedules
|
|
28
|
+
and the lastest time is the departure time of the first copy trip in the "late-shifted" copy schedules.
|
|
29
|
+
|
|
30
|
+
# If the vehicle's first trip is a non-copy trip, the earliest time is the departure time of the first trip. If the
|
|
31
|
+
# vehicle's last trip is a non-copy trip, the latest time is the departure time of the last trip.
|
|
32
|
+
|
|
33
|
+
:param dict_of_events: An ordered dictionary storing the data related to an event. The keys are the start times of
|
|
34
|
+
the events.
|
|
35
|
+
:param list_of_finished_trips: A list of finished trips of a vehicle directly from
|
|
36
|
+
:class:`eflips.depot.simple_vehicle.SimpleVehicle` object.
|
|
37
|
+
|
|
38
|
+
:param db_vehicle_id: The vehicle id in the database.
|
|
39
|
+
|
|
40
|
+
:return: A tuple of three elements. The first element is a list of finished schedules of the vehicle. The second and
|
|
41
|
+
third elements are the earliest and latest time of the vehicle's schedules.
|
|
42
|
+
"""
|
|
43
|
+
finished_schedules = []
|
|
44
|
+
|
|
45
|
+
list_of_finished_trips.sort(key=lambda x: x.atd)
|
|
46
|
+
earliest_time = None
|
|
47
|
+
latest_time = None
|
|
48
|
+
|
|
49
|
+
for i in range(len(list_of_finished_trips)):
|
|
50
|
+
assert list_of_finished_trips[i].atd == list_of_finished_trips[i].std, (
|
|
51
|
+
"The trip {current_trip.ID} is delayed. The simulation doesn't "
|
|
52
|
+
"support delayed trips for now."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if list_of_finished_trips[i].is_copy is False:
|
|
56
|
+
current_trip = list_of_finished_trips[i]
|
|
57
|
+
|
|
58
|
+
finished_schedules.append((int(current_trip.ID), db_vehicle_id))
|
|
59
|
+
dict_of_events[current_trip.atd] = {
|
|
60
|
+
"type": "Trip",
|
|
61
|
+
"id": int(current_trip.ID),
|
|
62
|
+
}
|
|
63
|
+
if i == 0:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"New Vehicle required for the trip {current_trip.ID}, which suggests the fleet or the "
|
|
66
|
+
f"infrastructure might not be enough for the full electrification. Please add charging "
|
|
67
|
+
f"interfaces or increase charging power ."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
elif i != 0 and i == len(list_of_finished_trips) - 1:
|
|
71
|
+
# Vehicle's last trip is a non-copy trip
|
|
72
|
+
if earliest_time is None:
|
|
73
|
+
earliest_time = list_of_finished_trips[i - 1].ata
|
|
74
|
+
latest_time = list_of_finished_trips[i].ata
|
|
75
|
+
|
|
76
|
+
else:
|
|
77
|
+
if list_of_finished_trips[i - 1].is_copy is True:
|
|
78
|
+
earliest_time = list_of_finished_trips[i - 1].ata
|
|
79
|
+
if list_of_finished_trips[i + 1].is_copy is True:
|
|
80
|
+
latest_time = list_of_finished_trips[i + 1].atd
|
|
81
|
+
|
|
82
|
+
return finished_schedules, earliest_time, latest_time
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def generate_vehicle_events(
|
|
86
|
+
dict_of_events,
|
|
87
|
+
current_vehicle: SimpleVehicle,
|
|
88
|
+
virtual_waiting_area_id: int,
|
|
89
|
+
earliest_time: datetime.datetime,
|
|
90
|
+
latest_time: datetime.datetime,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""
|
|
93
|
+
This function generates and ordered dictionary storing the data related to an event.
|
|
94
|
+
|
|
95
|
+
It returns a dictionary. The keys are the start times of the
|
|
96
|
+
events. The values are also dictionaries containing:
|
|
97
|
+
- type: The type of the event.
|
|
98
|
+
- end: The end time of the event.
|
|
99
|
+
- area: The area id of the event.
|
|
100
|
+
- slot: The slot id of the event.
|
|
101
|
+
- id: The id of the event-related process.
|
|
102
|
+
|
|
103
|
+
For trips, only the type is stored.
|
|
104
|
+
|
|
105
|
+
For waiting events, the slot is not stored for now.
|
|
106
|
+
|
|
107
|
+
:param current_vehicle: a :class:`eflips.depot.simple_vehicle.SimpleVehicle` object.
|
|
108
|
+
|
|
109
|
+
:param virtual_waiting_area_id: the id of the virtual waiting area. Vehicles waiting for the first process will park here.
|
|
110
|
+
|
|
111
|
+
:param earliest_time: the earliest relevant time of the current vehicle. Any events earlier than this will not be
|
|
112
|
+
handled.
|
|
113
|
+
|
|
114
|
+
:param latest_time: the latest relevant time of the current vehicle. Any events later than this will not be handled.
|
|
115
|
+
|
|
116
|
+
:return: None. The results are added to the dictionary.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
logger = logging.getLogger(__name__)
|
|
120
|
+
|
|
121
|
+
# For convenience
|
|
122
|
+
area_log = current_vehicle.logger.loggedData["dwd.current_area"]
|
|
123
|
+
slot_log = current_vehicle.logger.loggedData["dwd.current_slot"]
|
|
124
|
+
waiting_log = current_vehicle.logger.loggedData["area_waiting_time"]
|
|
125
|
+
|
|
126
|
+
# Handling waiting events
|
|
127
|
+
waiting_log_timekeys = sorted(waiting_log.keys())
|
|
128
|
+
|
|
129
|
+
for idx in range(len(waiting_log_timekeys)):
|
|
130
|
+
waiting_end_time = waiting_log_timekeys[idx]
|
|
131
|
+
|
|
132
|
+
# Only extract events if the time is within the upper mentioned range
|
|
133
|
+
|
|
134
|
+
if earliest_time <= waiting_end_time <= latest_time:
|
|
135
|
+
waiting_info = waiting_log[waiting_end_time]
|
|
136
|
+
|
|
137
|
+
if waiting_info["waiting_time"] == 0:
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
logger.info(
|
|
141
|
+
f"Vehicle {current_vehicle.ID} has been waiting for {waiting_info['waiting_time']} seconds. "
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
start_time = waiting_end_time - waiting_info["waiting_time"]
|
|
145
|
+
|
|
146
|
+
if waiting_info["area"] == waiting_log[waiting_log_timekeys[0]]["area"]:
|
|
147
|
+
# if the vehicle is waiting for the first process, put it in the virtual waiting area
|
|
148
|
+
waiting_area_id = virtual_waiting_area_id
|
|
149
|
+
else:
|
|
150
|
+
# If the vehicle is waiting for other processes,
|
|
151
|
+
# put it in the area of the prodecessor process of the waited process.
|
|
152
|
+
waiting_area_id = waiting_log[waiting_log_timekeys[idx - 1]]["area"]
|
|
153
|
+
|
|
154
|
+
dict_of_events[start_time] = {
|
|
155
|
+
"type": "Standby",
|
|
156
|
+
"end": waiting_end_time,
|
|
157
|
+
"area": waiting_area_id,
|
|
158
|
+
"is_waiting": True,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Create a list of battery log in order of time asc. Convenient for looking up corresponding soc
|
|
162
|
+
|
|
163
|
+
for time_stamp, process_log in current_vehicle.logger.loggedData[
|
|
164
|
+
"dwd.active_processes_copy"
|
|
165
|
+
].items():
|
|
166
|
+
if earliest_time <= time_stamp <= latest_time:
|
|
167
|
+
num_process = len(process_log)
|
|
168
|
+
if num_process == 0:
|
|
169
|
+
# A departure happens and this trip should already be stored in the dictionary
|
|
170
|
+
pass
|
|
171
|
+
else:
|
|
172
|
+
for process in process_log:
|
|
173
|
+
current_area = area_log[time_stamp]
|
|
174
|
+
current_slot = slot_log[time_stamp]
|
|
175
|
+
|
|
176
|
+
if current_area is None or current_slot is None:
|
|
177
|
+
raise ValueError(
|
|
178
|
+
f"For process {process.ID} Area and slot should not be None."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
match process.status:
|
|
182
|
+
case ProcessStatus.COMPLETED | ProcessStatus.CANCELLED:
|
|
183
|
+
assert (
|
|
184
|
+
len(process.starts) == 1 and len(process.ends) == 1
|
|
185
|
+
), (
|
|
186
|
+
f"Current process {process.ID} is completed and should only contain one start and "
|
|
187
|
+
f"one end time."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if process.dur > 0:
|
|
191
|
+
# Valid duration
|
|
192
|
+
dict_of_events[time_stamp] = {
|
|
193
|
+
"type": type(process).__name__,
|
|
194
|
+
"end": process.ends[0],
|
|
195
|
+
"area": current_area.ID,
|
|
196
|
+
"slot": current_slot,
|
|
197
|
+
"id": process.ID,
|
|
198
|
+
}
|
|
199
|
+
else:
|
|
200
|
+
# Duration is 0
|
|
201
|
+
assert current_area.issink is True, (
|
|
202
|
+
f"A process with no duration could only "
|
|
203
|
+
f"happen in the last area before dispatched"
|
|
204
|
+
)
|
|
205
|
+
if (
|
|
206
|
+
time_stamp in dict_of_events.keys()
|
|
207
|
+
and "end" in dict_of_events[time_stamp].keys()
|
|
208
|
+
):
|
|
209
|
+
start_this_event = dict_of_events[time_stamp]["end"]
|
|
210
|
+
if start_this_event in dict_of_events.keys():
|
|
211
|
+
if (
|
|
212
|
+
dict_of_events[start_this_event]["type"]
|
|
213
|
+
== "Trip"
|
|
214
|
+
):
|
|
215
|
+
logger.info(
|
|
216
|
+
f"Vehicle {current_vehicle.ID} must depart immediately after charged. "
|
|
217
|
+
f"Thus there will be no STANDBY_DEPARTURE event."
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
else:
|
|
221
|
+
raise ValueError(
|
|
222
|
+
f"There is already an event "
|
|
223
|
+
f"{dict_of_events[start_this_event]} at {start_this_event}."
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
dict_of_events[start_this_event] = {
|
|
229
|
+
"type": type(process).__name__,
|
|
230
|
+
"area": current_area.ID,
|
|
231
|
+
"slot": current_slot,
|
|
232
|
+
"id": process.ID,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
case ProcessStatus.IN_PROGRESS:
|
|
236
|
+
assert (
|
|
237
|
+
len(process.starts) == 1 and len(process.ends) == 0
|
|
238
|
+
), f"Current process {process.ID} is marked IN_PROGRESS, but has an end."
|
|
239
|
+
|
|
240
|
+
if current_area is None or current_slot is None:
|
|
241
|
+
raise ValueError(
|
|
242
|
+
f"For process {process.ID} Area and slot should not be None."
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if process.dur > 0:
|
|
246
|
+
# Valid duration
|
|
247
|
+
dict_of_events[time_stamp] = {
|
|
248
|
+
"type": type(process).__name__,
|
|
249
|
+
"end": process.etc,
|
|
250
|
+
"area": current_area.ID,
|
|
251
|
+
"slot": current_slot,
|
|
252
|
+
"id": process.ID,
|
|
253
|
+
}
|
|
254
|
+
else:
|
|
255
|
+
raise NotImplementedError(
|
|
256
|
+
"We believe this should never happen. If it happens, handle it here."
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# The following ProcessStatus possibly only happen while the simulation is running,
|
|
260
|
+
# not in the results
|
|
261
|
+
case ProcessStatus.WAITING:
|
|
262
|
+
raise NotImplementedError(
|
|
263
|
+
f"Current process {process.ID} is waiting. Not implemented yet."
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
case ProcessStatus.NOT_STARTED:
|
|
267
|
+
raise NotImplementedError(
|
|
268
|
+
f"Current process {process.ID} is not started. Not implemented yet."
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
case _:
|
|
272
|
+
raise ValueError(
|
|
273
|
+
f"Invalid process status {process.status} for process {process.ID}."
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def complete_standby_departure_events(
|
|
278
|
+
dict_of_events: Dict, latest_time: datetime.datetime
|
|
279
|
+
) -> None:
|
|
280
|
+
"""
|
|
281
|
+
This function completes the standby departure events by adding an end time to each standby departure event.
|
|
282
|
+
|
|
283
|
+
:param dict_of_events: a dictionary containing the events of a vehicle. The keys are the start times of the events.
|
|
284
|
+
|
|
285
|
+
:param latest_time: the latest relevant time of the current vehicle. Any events later than this will not be handled.
|
|
286
|
+
|
|
287
|
+
:return: None. The results are added to the dictionary.
|
|
288
|
+
"""
|
|
289
|
+
for i in range(len(dict_of_events.keys())):
|
|
290
|
+
time_keys = sorted(dict_of_events.keys())
|
|
291
|
+
|
|
292
|
+
process_dict = dict_of_events[time_keys[i]]
|
|
293
|
+
if "end" not in process_dict and process_dict["type"] != "Trip":
|
|
294
|
+
# End time of a standby_departure will be the start of the following trip
|
|
295
|
+
if i == len(time_keys) - 1:
|
|
296
|
+
# The event reaches simulation end
|
|
297
|
+
end_time = latest_time
|
|
298
|
+
else:
|
|
299
|
+
end_time = time_keys[i + 1]
|
|
300
|
+
|
|
301
|
+
process_dict["end"] = end_time
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def add_soc_to_events(dict_of_events, battery_log) -> None:
|
|
305
|
+
"""
|
|
306
|
+
This function completes the soc of each event by looking up the battery log.
|
|
307
|
+
|
|
308
|
+
:param dict_of_events: a dictionary containing the events of a vehicle. The keys are the start times of the events.
|
|
309
|
+
|
|
310
|
+
:param battery_log: a list of battery logs of a vehicle.
|
|
311
|
+
|
|
312
|
+
:return: None. The results are added to the dictionary.
|
|
313
|
+
"""
|
|
314
|
+
battery_log_list = []
|
|
315
|
+
for log in battery_log:
|
|
316
|
+
battery_log_list.append((log.t, log.energy / log.energy_real))
|
|
317
|
+
|
|
318
|
+
time_keys = sorted(dict_of_events.keys())
|
|
319
|
+
for i in range(len(time_keys)):
|
|
320
|
+
# Get soc
|
|
321
|
+
soc_start = None
|
|
322
|
+
soc_end = None
|
|
323
|
+
start_time = time_keys[i]
|
|
324
|
+
process_dict = dict_of_events[time_keys[i]]
|
|
325
|
+
for j in range(len(battery_log_list)):
|
|
326
|
+
# Access the correct battery log according to time since there is only one battery log for each time
|
|
327
|
+
log = battery_log_list[j]
|
|
328
|
+
|
|
329
|
+
if process_dict["type"] != "Trip":
|
|
330
|
+
if log[0] == start_time:
|
|
331
|
+
soc_start = log[1]
|
|
332
|
+
if log[0] == process_dict["end"]:
|
|
333
|
+
soc_end = log[1]
|
|
334
|
+
if log[0] < start_time < battery_log_list[j + 1][0]:
|
|
335
|
+
soc_start = log[1]
|
|
336
|
+
if log[0] < process_dict["end"] < battery_log_list[j + 1][0]:
|
|
337
|
+
soc_end = log[1]
|
|
338
|
+
|
|
339
|
+
if soc_start is not None:
|
|
340
|
+
soc_start = min(soc_start, 1) # so
|
|
341
|
+
process_dict["soc_start"] = soc_start
|
|
342
|
+
if soc_end is not None:
|
|
343
|
+
soc_end = min(soc_end, 1) # soc should not exceed 1
|
|
344
|
+
process_dict["soc_end"] = soc_end
|
|
345
|
+
|
|
346
|
+
else:
|
|
347
|
+
continue
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def add_events_into_database(
|
|
351
|
+
db_vehicle, dict_of_events, session, scenario, simulation_start_time
|
|
352
|
+
) -> None:
|
|
353
|
+
"""
|
|
354
|
+
This function generates :class:`eflips.model.Event` objects from the dictionary of events and adds them into the.
|
|
355
|
+
|
|
356
|
+
database.
|
|
357
|
+
|
|
358
|
+
:param db_vehicle: vehicle object in the database
|
|
359
|
+
|
|
360
|
+
:param dict_of_events: dictionary containing the events of a vehicle. The keys are the start times of the events.
|
|
361
|
+
|
|
362
|
+
:param session: a :class:`sqlalchemy.orm.Session` object for database connection.
|
|
363
|
+
|
|
364
|
+
:param scenario: the current simulated scenario
|
|
365
|
+
|
|
366
|
+
:param simulation_start_time: simulation start time in :class:`datetime.datetime` format
|
|
367
|
+
|
|
368
|
+
:return: None. The results are added to the database.
|
|
369
|
+
"""
|
|
370
|
+
logger = logging.getLogger(__name__)
|
|
371
|
+
|
|
372
|
+
for start_time, process_dict in dict_of_events.items():
|
|
373
|
+
# Generate EventType
|
|
374
|
+
match process_dict["type"]:
|
|
375
|
+
case "Serve":
|
|
376
|
+
event_type = EventType.SERVICE
|
|
377
|
+
case "Charge":
|
|
378
|
+
event_type = EventType.CHARGING_DEPOT
|
|
379
|
+
case "Standby":
|
|
380
|
+
if (
|
|
381
|
+
"is_waiting" in process_dict.keys()
|
|
382
|
+
and process_dict["is_waiting"] is True
|
|
383
|
+
):
|
|
384
|
+
event_type = EventType.STANDBY
|
|
385
|
+
else:
|
|
386
|
+
event_type = EventType.STANDBY_DEPARTURE
|
|
387
|
+
case "Precondition":
|
|
388
|
+
event_type = EventType.PRECONDITIONING
|
|
389
|
+
case "Trip":
|
|
390
|
+
continue
|
|
391
|
+
case _:
|
|
392
|
+
raise ValueError(
|
|
393
|
+
'Invalid process type %s. Valid process types are "Serve", "Charge", '
|
|
394
|
+
'"Standby", "Precondition"'
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
if process_dict["end"] == start_time:
|
|
398
|
+
logger.warning("Refusing to create an event with zero duration.")
|
|
399
|
+
continue
|
|
400
|
+
|
|
401
|
+
current_event = Event(
|
|
402
|
+
scenario=scenario,
|
|
403
|
+
vehicle_type_id=db_vehicle.vehicle_type_id,
|
|
404
|
+
vehicle=db_vehicle,
|
|
405
|
+
station_id=None,
|
|
406
|
+
area_id=int(process_dict["area"]),
|
|
407
|
+
subloc_no=int(process_dict["slot"]) - 1
|
|
408
|
+
if "slot" in process_dict.keys()
|
|
409
|
+
else 00,
|
|
410
|
+
trip_id=None,
|
|
411
|
+
time_start=timedelta(seconds=start_time) + simulation_start_time,
|
|
412
|
+
time_end=timedelta(seconds=process_dict["end"]) + simulation_start_time,
|
|
413
|
+
soc_start=process_dict["soc_start"]
|
|
414
|
+
if process_dict["soc_start"] is not None
|
|
415
|
+
else process_dict["soc_end"],
|
|
416
|
+
soc_end=process_dict["soc_end"]
|
|
417
|
+
if process_dict["soc_end"] is not None
|
|
418
|
+
else process_dict["soc_start"], # if only one battery log is found,
|
|
419
|
+
# then this is not an event with soc change
|
|
420
|
+
event_type=event_type,
|
|
421
|
+
description=process_dict["id"] if "id" in process_dict.keys() else None,
|
|
422
|
+
timeseries=None,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
session.add(current_event)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def update_vehicle_in_rotation(session, scenario, list_of_assigned_schedules) -> None:
|
|
429
|
+
"""
|
|
430
|
+
This function updates the vehicle id assigned to the rotations and deletes the events that are not depot events.
|
|
431
|
+
|
|
432
|
+
:param session: a :class:`sqlalchemy.orm.Session` object for database connection.
|
|
433
|
+
:param scenario: the current simulated scenario
|
|
434
|
+
:param list_of_assigned_schedules: a list of tuples containing the rotation id and the vehicle id.
|
|
435
|
+
:return: None. The results are added to the database.
|
|
436
|
+
"""
|
|
437
|
+
# New rotation assignment
|
|
438
|
+
for schedule_id, vehicle_id in list_of_assigned_schedules:
|
|
439
|
+
# Get corresponding old vehicle id
|
|
440
|
+
session.query(Rotation).filter(Rotation.id == schedule_id).update(
|
|
441
|
+
{"vehicle_id": vehicle_id}, synchronize_session="auto"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# Delete all non-depot events
|
|
445
|
+
session.query(Event).filter(
|
|
446
|
+
Event.scenario == scenario,
|
|
447
|
+
Event.trip_id.isnot(None) | Event.station_id.isnot(None),
|
|
448
|
+
).delete(synchronize_session="auto")
|
|
449
|
+
|
|
450
|
+
session.flush()
|
|
451
|
+
|
|
452
|
+
# Delete all vehicles without rotations
|
|
453
|
+
vehicle_assigned_sq = (
|
|
454
|
+
session.query(Rotation.vehicle_id)
|
|
455
|
+
.filter(Rotation.scenario == scenario)
|
|
456
|
+
.distinct()
|
|
457
|
+
.subquery()
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
session.query(Vehicle).filter(Vehicle.scenario == scenario).filter(
|
|
461
|
+
Vehicle.id.not_in(select(vehicle_assigned_sq))
|
|
462
|
+
).delete()
|
|
463
|
+
|
|
464
|
+
session.flush()
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def update_waiting_events(session, scenario, waiting_area_id) -> None:
|
|
468
|
+
"""
|
|
469
|
+
This function evaluates the capacity of waiting area and assigns the waiting events to corresponding slots in the.
|
|
470
|
+
|
|
471
|
+
waiting area.
|
|
472
|
+
|
|
473
|
+
:param session: a :class:`sqlalchemy.orm.Session` object for database connection.
|
|
474
|
+
|
|
475
|
+
:param scenario: the current simulated scenario.
|
|
476
|
+
|
|
477
|
+
:param waiting_area_id: id of the waiting area.
|
|
478
|
+
|
|
479
|
+
:raise ValueError: if the waiting area capacity is less than the peak waiting occupancy.
|
|
480
|
+
|
|
481
|
+
:return: None. The results are added to the database.
|
|
482
|
+
"""
|
|
483
|
+
logger = logging.getLogger(__name__)
|
|
484
|
+
|
|
485
|
+
# Process all the STANDBY (waiting) events #
|
|
486
|
+
all_waiting_starts = (
|
|
487
|
+
session.query(Event)
|
|
488
|
+
.filter(
|
|
489
|
+
Event.scenario_id == scenario.id,
|
|
490
|
+
Event.event_type == EventType.STANDBY,
|
|
491
|
+
Event.area_id == waiting_area_id,
|
|
492
|
+
)
|
|
493
|
+
.all()
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
all_waiting_ends = (
|
|
497
|
+
session.query(Event)
|
|
498
|
+
.filter(
|
|
499
|
+
Event.scenario_id == scenario.id,
|
|
500
|
+
Event.event_type == EventType.STANDBY,
|
|
501
|
+
Event.area_id == waiting_area_id,
|
|
502
|
+
)
|
|
503
|
+
.all()
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
assert len(all_waiting_starts) == len(
|
|
507
|
+
all_waiting_ends
|
|
508
|
+
), f"Number of waiting events starts {len(all_waiting_starts)} is not equal to the number of waiting event ends"
|
|
509
|
+
|
|
510
|
+
if len(all_waiting_starts) == 0:
|
|
511
|
+
logger.info(
|
|
512
|
+
"No waiting events found. The depot has enough capacity for waiting. Change the waiting area capacity to 10 as buffer."
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
session.query(Area).filter(Area.id == waiting_area_id).update(
|
|
516
|
+
{"capacity": 10}, synchronize_session="auto"
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
return
|
|
520
|
+
|
|
521
|
+
list_waiting_timestamps = []
|
|
522
|
+
for waiting_start in all_waiting_starts:
|
|
523
|
+
list_waiting_timestamps.append(
|
|
524
|
+
{"timestamp": waiting_start.time_start, "event": (waiting_start.id, 1)}
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
for waiting_end in all_waiting_ends:
|
|
528
|
+
list_waiting_timestamps.append(
|
|
529
|
+
{"timestamp": waiting_end.time_end, "event": (waiting_end.id, -1)}
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
list_waiting_timestamps.sort(key=lambda x: x["timestamp"])
|
|
533
|
+
start_and_end_records = [wt["event"][1] for wt in list_waiting_timestamps]
|
|
534
|
+
|
|
535
|
+
peak_waiting_occupancy = max(list(itertools.accumulate(start_and_end_records)))
|
|
536
|
+
|
|
537
|
+
# Assuming that there is only one waiting area in each depot
|
|
538
|
+
|
|
539
|
+
waiting_area_id = all_waiting_starts[0].area_id
|
|
540
|
+
waiting_area = session.query(Area).filter(Area.id == waiting_area_id).first()
|
|
541
|
+
if waiting_area.capacity > peak_waiting_occupancy:
|
|
542
|
+
logger.info(
|
|
543
|
+
f"Current waiting area capacity {waiting_area.capacity} "
|
|
544
|
+
f"is greater than the peak waiting occupancy. Updating the capacity to {peak_waiting_occupancy}."
|
|
545
|
+
)
|
|
546
|
+
session.query(Area).filter(Area.id == waiting_area_id).update(
|
|
547
|
+
{"capacity": peak_waiting_occupancy}, synchronize_session="auto"
|
|
548
|
+
)
|
|
549
|
+
session.flush()
|
|
550
|
+
elif waiting_area.capacity < peak_waiting_occupancy:
|
|
551
|
+
raise ValueError(
|
|
552
|
+
f"Waiting area capacity is less than the peak waiting occupancy. "
|
|
553
|
+
f"Waiting area capacity: {waiting_area.capacity}, peak waiting occupancy: {peak_waiting_occupancy}."
|
|
554
|
+
)
|
|
555
|
+
else:
|
|
556
|
+
pass
|
|
557
|
+
|
|
558
|
+
session.flush()
|
|
559
|
+
|
|
560
|
+
# Update waiting slots
|
|
561
|
+
virtual_waiting_area = [None] * peak_waiting_occupancy
|
|
562
|
+
for wt in list_waiting_timestamps:
|
|
563
|
+
# check in
|
|
564
|
+
if wt["event"][1] == 1:
|
|
565
|
+
for i in range(len(virtual_waiting_area)):
|
|
566
|
+
if virtual_waiting_area[i] is None:
|
|
567
|
+
virtual_waiting_area[i] = wt["event"][0]
|
|
568
|
+
session.query(Event).filter(Event.id == wt["event"][0]).update(
|
|
569
|
+
{"subloc_no": i}, synchronize_session="auto"
|
|
570
|
+
)
|
|
571
|
+
break
|
|
572
|
+
# check out
|
|
573
|
+
else:
|
|
574
|
+
for i in range(len(virtual_waiting_area)):
|
|
575
|
+
if virtual_waiting_area[i] == wt["event"][0]:
|
|
576
|
+
current_waiting_event = (
|
|
577
|
+
session.query(Event).filter(Event.id == wt["event"][0]).first()
|
|
578
|
+
)
|
|
579
|
+
assert current_waiting_event.subloc_no == i, (
|
|
580
|
+
f"Subloc number of the event {current_waiting_event.id} is not equal to the index of the "
|
|
581
|
+
f"event in the virtual waiting area."
|
|
582
|
+
)
|
|
583
|
+
virtual_waiting_area[i] = None
|
|
584
|
+
break
|
|
585
|
+
|
|
586
|
+
session.flush()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: eflips-depot
|
|
3
|
-
Version: 4.3.
|
|
3
|
+
Version: 4.3.18
|
|
4
4
|
Summary: Depot Simulation for eFLIPS
|
|
5
5
|
Home-page: https://github.com/mpm-tu-berlin/eflips-depot
|
|
6
6
|
License: AGPL-3.0-or-later
|
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
16
|
Requires-Dist: eflips (>=0.1.3,<0.2.0)
|
|
16
17
|
Requires-Dist: eflips-model (>=5.0.0,<6.0.0)
|
|
17
18
|
Requires-Dist: pandas (>=2.1.4,<3.0.0)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
eflips/depot/__init__.py,sha256=n7jte8R6j_Ad4Mp4hkklKwil5r8u8Q_SbXrCC-nf5jM,1556
|
|
2
|
-
eflips/depot/api/__init__.py,sha256=
|
|
2
|
+
eflips/depot/api/__init__.py,sha256=9Oc-yauxq4hDqojnaflIt0ecTtKtUBcxy1pbz9a4aK0,57128
|
|
3
3
|
eflips/depot/api/defaults/default_settings.json,sha256=0eUDTw_rtLQFvthP8oJL93iRXlmAOravAg-4qqGMQAY,5375
|
|
4
4
|
eflips/depot/api/private/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
eflips/depot/api/private/depot.py,sha256=
|
|
5
|
+
eflips/depot/api/private/depot.py,sha256=Fan7vyVDNcQmuKxoETI6FqLEYII-TJlSor2wiT79zNc,35344
|
|
6
|
+
eflips/depot/api/private/results_to_database.py,sha256=HrElB_ctOZX-Jir4uab76ii26H2LOdwpFTCmNmLawHg,24223
|
|
6
7
|
eflips/depot/api/private/smart_charging.py,sha256=bgxBJ-Lytd2JXN_fm8wb268wbRDrwur_zfEzj0cnhu4,13052
|
|
7
8
|
eflips/depot/api/private/util.py,sha256=Ye-WXNzHcNfunFijK7FCIU3AiCuMg83KnEhnKbtlZu8,17242
|
|
8
9
|
eflips/depot/configuration.py,sha256=Op3hlir-dEN7yHr0kTqbYANoCBKFWK6uKOv3NJl8w_w,35678
|
|
@@ -35,7 +36,7 @@ eflips/depot/simulation.py,sha256=ee0qTzOzG-8ybN36ie_NJallXfC7jUaS9JZvaYFziLs,10
|
|
|
35
36
|
eflips/depot/smart_charging.py,sha256=C3BYqzn2-OYY4ipXm0ETtavbAM9QXZMYULBpVoChf0E,54311
|
|
36
37
|
eflips/depot/standalone.py,sha256=VxcTzBaB67fNJUMmjPRwKXjhqTy6oQ41Coote2LvAmk,22338
|
|
37
38
|
eflips/depot/validation.py,sha256=TIuY7cQtEJI4H2VVMSuY5IIVkacEEZ67weeMuY3NSAM,7097
|
|
38
|
-
eflips_depot-4.3.
|
|
39
|
-
eflips_depot-4.3.
|
|
40
|
-
eflips_depot-4.3.
|
|
41
|
-
eflips_depot-4.3.
|
|
39
|
+
eflips_depot-4.3.18.dist-info/LICENSE.md,sha256=KB4XTk1fPHjtZCYDyPyreu6h1LVJVZXYg-5vePcWZAc,34143
|
|
40
|
+
eflips_depot-4.3.18.dist-info/METADATA,sha256=KHbavT4VryoPtummTw2wS9nxHKUsHgyeLcF_dMWJ0EI,5891
|
|
41
|
+
eflips_depot-4.3.18.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
42
|
+
eflips_depot-4.3.18.dist-info/RECORD,,
|
|
File without changes
|