eflips-depot 3.2.7__py3-none-any.whl → 4.0.1__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 +679 -417
- eflips/depot/api/private/depot.py +57 -20
- eflips/depot/api/private/smart_charging.py +39 -37
- eflips/depot/api/private/util.py +43 -1
- {eflips_depot-3.2.7.dist-info → eflips_depot-4.0.1.dist-info}/METADATA +2 -2
- {eflips_depot-3.2.7.dist-info → eflips_depot-4.0.1.dist-info}/RECORD +8 -8
- {eflips_depot-3.2.7.dist-info → eflips_depot-4.0.1.dist-info}/LICENSE.md +0 -0
- {eflips_depot-3.2.7.dist-info → eflips_depot-4.0.1.dist-info}/WHEEL +0 -0
eflips/depot/api/__init__.py
CHANGED
|
@@ -25,12 +25,15 @@ The following steps are recommended for using the API:
|
|
|
25
25
|
b. Run the :func:`simple_consumption_simulation` function again, this time with ``initialize_vehicles=False``.
|
|
26
26
|
"""
|
|
27
27
|
import copy
|
|
28
|
+
import datetime
|
|
29
|
+
import itertools
|
|
28
30
|
import os
|
|
29
31
|
import warnings
|
|
30
32
|
from datetime import timedelta
|
|
31
33
|
from enum import Enum
|
|
32
34
|
from math import ceil
|
|
33
|
-
from
|
|
35
|
+
from collections import OrderedDict
|
|
36
|
+
from typing import Any, Dict, Optional, Union, List
|
|
34
37
|
|
|
35
38
|
import numpy as np
|
|
36
39
|
import sqlalchemy.orm
|
|
@@ -43,15 +46,12 @@ from eflips.model import (
|
|
|
43
46
|
Scenario,
|
|
44
47
|
Trip,
|
|
45
48
|
Vehicle,
|
|
46
|
-
Process,
|
|
47
|
-
AssocAreaProcess,
|
|
48
|
-
Station,
|
|
49
49
|
)
|
|
50
50
|
from sqlalchemy.orm import Session
|
|
51
51
|
from sqlalchemy.sql import select
|
|
52
52
|
|
|
53
53
|
import eflips.depot
|
|
54
|
-
from eflips.depot import DepotEvaluation, ProcessStatus, SimulationHost
|
|
54
|
+
from eflips.depot import DepotEvaluation, ProcessStatus, SimulationHost, SimpleVehicle
|
|
55
55
|
from eflips.depot.api.private.depot import (
|
|
56
56
|
create_simple_depot,
|
|
57
57
|
delete_depots,
|
|
@@ -65,6 +65,7 @@ from eflips.depot.api.private.util import (
|
|
|
65
65
|
start_and_end_times,
|
|
66
66
|
vehicle_type_to_global_constants_dict,
|
|
67
67
|
VehicleSchedule,
|
|
68
|
+
check_depot_validity,
|
|
68
69
|
)
|
|
69
70
|
|
|
70
71
|
|
|
@@ -111,20 +112,24 @@ def simple_consumption_simulation(
|
|
|
111
112
|
entries and ``Rotation.vehicle_id`` is already set.
|
|
112
113
|
|
|
113
114
|
:param scenario: Either a :class:`eflips.model.Scenario` object containing the input data for the simulation. Or
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
an integer specifying the ID of a scenario in the database. Or any other object that has an attribute
|
|
116
|
+
``id`` that is an integer. If no :class:`eflips.model.Scenario` object is passed, the ``database_url``
|
|
117
|
+
parameter must be set to a valid database URL ot the environment variable ``DATABASE_URL`` must be set to a
|
|
118
|
+
valid database URL.
|
|
119
|
+
|
|
118
120
|
:param initialize_vehicles: A boolean flag indicating whether the vehicles should be initialized in the database.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
When running this function for the first time, this should be set to True. When running this function again
|
|
122
|
+
after the vehicles have been initialized, this should be set to False.
|
|
123
|
+
|
|
121
124
|
:param database_url: An optional database URL. If no database URL is passed and the `scenario` parameter is not a
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
:class:`eflips.model.Scenario` object, the environment variable `DATABASE_URL` must be set to a
|
|
126
|
+
valid database URL.
|
|
127
|
+
|
|
124
128
|
:param calculate_timeseries: A boolean flag indicating whether the timeseries should be calculated. If this is set
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
to True, the SoC at each stop is calculated and added to the "timeseries" column of the Event table. If this
|
|
130
|
+
is set to False, the "timeseries" column of the Event table will be set to ``None``. Setting this to false
|
|
131
|
+
may significantly speed up the simulation.
|
|
132
|
+
|
|
128
133
|
:return: Nothing. The results are added to the database.
|
|
129
134
|
"""
|
|
130
135
|
with create_session(scenario, database_url) as (session, scenario):
|
|
@@ -309,7 +314,9 @@ def generate_depot_layout(
|
|
|
309
314
|
``id`` that is an integer. If no :class:`eflips.model.Scenario` object is passed, the ``database_url``
|
|
310
315
|
parameter must be set to a valid database URL ot the environment variable ``DATABASE_URL`` must be set to a
|
|
311
316
|
valid database URL.
|
|
317
|
+
|
|
312
318
|
:param charging_power: the charging power of the charging area in kW
|
|
319
|
+
|
|
313
320
|
:param delete_existing_depot: if there is already a depot existing in this scenario, set True to delete this
|
|
314
321
|
existing depot. Set to False and a ValueError will be raised if there is a depot in this scenario.
|
|
315
322
|
|
|
@@ -376,6 +383,7 @@ def generate_depot_layout(
|
|
|
376
383
|
charging_power=charging_power,
|
|
377
384
|
session=session,
|
|
378
385
|
cleaning_duration=timedelta(seconds=CLEAN_DURATION),
|
|
386
|
+
safety_margin=0.2,
|
|
379
387
|
)
|
|
380
388
|
|
|
381
389
|
|
|
@@ -387,16 +395,18 @@ def apply_even_smart_charging(
|
|
|
387
395
|
"""
|
|
388
396
|
Takes a scenario where depot simulation has been run and applies smart charging to the depot.
|
|
389
397
|
|
|
390
|
-
This modifies the
|
|
391
|
-
|
|
392
|
-
not modified.
|
|
398
|
+
This modifies the time and power of the charging events in the database. The arrival and departure times and SoCs at
|
|
399
|
+
these times are not modified.
|
|
393
400
|
|
|
394
401
|
:param scenario: A :class:`eflips.model.Scenario` object containing the input data for the simulation.
|
|
402
|
+
|
|
395
403
|
:param database_url: An optional database URL. If no database URL is passed and the `scenario` parameter is not a
|
|
396
404
|
:class:`eflips.model.Scenario` object, the environment variable `DATABASE_URL` must be set to a valid database
|
|
397
405
|
URL.
|
|
406
|
+
|
|
398
407
|
:param standby_departure_duration: The duration of the STANDBY_DEPARTURE event. This is the time the vehicle is
|
|
399
408
|
allowed to wait at the depot before it has to leave. The default is 5 minutes.
|
|
409
|
+
|
|
400
410
|
:return: None. The results are added to the database.
|
|
401
411
|
"""
|
|
402
412
|
with create_session(scenario, database_url) as (session, scenario):
|
|
@@ -480,14 +490,17 @@ def simulate_scenario(
|
|
|
480
490
|
``id`` that is an integer. If no :class:`eflips.model.Scenario` object is passed, the ``database_url``
|
|
481
491
|
parameter must be set to a valid database URL ot the environment variable ``DATABASE_URL`` must be set to a
|
|
482
492
|
valid database URL.
|
|
493
|
+
|
|
483
494
|
:param repetition_period: An optional timedelta object specifying the period of the vehicle schedules. This
|
|
484
495
|
is needed because the result should be a steady-state result. THis can only be achieved by simulating a
|
|
485
496
|
time period before and after our actual simulation, and then only using the "middle". eFLIPS tries to
|
|
486
497
|
automatically detect whether the schedule should be repeated daily or weekly. If this fails, a ValueError is
|
|
487
498
|
raised and repetition needs to be specified manually.
|
|
499
|
+
|
|
488
500
|
:param database_url: An optional database URL. If no database URL is passed and the `scenario` parameter is not a
|
|
489
501
|
:class:`eflips.model.Scenario` object, the environment variable `DATABASE_URL` must be set to a valid database
|
|
490
502
|
URL.
|
|
503
|
+
|
|
491
504
|
:param smart_charging_strategy: An optional parameter specifying the smart charging strategy to be used. The
|
|
492
505
|
default is SmartChargingStragegy.NONE. The following strategies are available:
|
|
493
506
|
- SmartChargingStragegy.NONE: Do not use smart charging. Buses are charged with the maximum power available,
|
|
@@ -541,7 +554,9 @@ def init_simulation(
|
|
|
541
554
|
The simulation host object can then be passed to :func:`run_simulation()`.
|
|
542
555
|
|
|
543
556
|
:param scenario: A :class:`eflips.model.Scenario` object containing the input data for the simulation.
|
|
557
|
+
|
|
544
558
|
:param session: A SQLAlchemy session object.
|
|
559
|
+
|
|
545
560
|
:param repetition_period: An optional timedelta object specifying the period of the vehicle schedules. This
|
|
546
561
|
is needed because the *result* should be a steady-state result. THis can only be achieved by simulating a
|
|
547
562
|
time period before and after our actual simulation, and then only using the "middle". eFLIPS tries to
|
|
@@ -549,7 +564,7 @@ def init_simulation(
|
|
|
549
564
|
raised and repetition needs to be specified manually.
|
|
550
565
|
|
|
551
566
|
:param vehicle_count_dict: An optional dictionary specifying the number of vehicles for each vehicle type for each
|
|
552
|
-
|
|
567
|
+
depot. The dictionary should have the following structure:
|
|
553
568
|
|
|
554
569
|
::
|
|
555
570
|
|
|
@@ -576,6 +591,9 @@ def init_simulation(
|
|
|
576
591
|
# Step 1: Set up the depot
|
|
577
592
|
eflips_depots = []
|
|
578
593
|
for depot in scenario.depots:
|
|
594
|
+
# Step 1.5: Check validity of a depot
|
|
595
|
+
check_depot_validity(depot)
|
|
596
|
+
|
|
579
597
|
depot_dict = depot_to_template(depot)
|
|
580
598
|
eflips_depots.append(
|
|
581
599
|
eflips.depot.Depotinput(filename_template=depot_dict, show_gui=False)
|
|
@@ -648,10 +666,12 @@ def init_simulation(
|
|
|
648
666
|
depot_id = str(depot.id)
|
|
649
667
|
eflips.globalConstants["depot"]["vehicle_count"][depot_id] = {}
|
|
650
668
|
vehicle_types_for_depot = set(str(area.vehicle_type_id) for area in depot.areas)
|
|
669
|
+
if "None" in vehicle_types_for_depot:
|
|
670
|
+
vehicle_types_for_depot.remove("None")
|
|
651
671
|
|
|
652
672
|
# If we have a vehicle count dictionary, we validate and use ir
|
|
653
673
|
if vehicle_count_dict is not None and depot_id in vehicle_count_dict.keys():
|
|
654
|
-
if vehicle_count_dict[depot_id].keys()
|
|
674
|
+
if set(vehicle_count_dict[depot_id].keys()) < vehicle_types_for_depot:
|
|
655
675
|
raise ValueError(
|
|
656
676
|
"The vehicle count dictionary does not contain all vehicle types for depot {depot_id}."
|
|
657
677
|
)
|
|
@@ -659,17 +679,21 @@ def init_simulation(
|
|
|
659
679
|
depot_id
|
|
660
680
|
] = vehicle_count_dict[depot_id]
|
|
661
681
|
else:
|
|
662
|
-
# Calculate it from the size of the
|
|
663
|
-
|
|
682
|
+
# Calculate it from the size of the charging area with a 2x margin
|
|
683
|
+
|
|
664
684
|
for vehicle_type in vehicle_types_for_depot:
|
|
665
|
-
vehicle_count =
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
685
|
+
vehicle_count = 0
|
|
686
|
+
for area in depot.areas:
|
|
687
|
+
if area.vehicle_type_id == int(vehicle_type):
|
|
688
|
+
# TODO potential edit if we make vehicle type of an area a list
|
|
689
|
+
for p in area.processes:
|
|
690
|
+
if p.electric_power is not None and p.duration is None:
|
|
691
|
+
vehicle_count = area.capacity
|
|
692
|
+
|
|
693
|
+
assert (
|
|
694
|
+
vehicle_count > 0
|
|
695
|
+
), f"The charging area capacity for vehicle type {vehicle_type} should not be 0."
|
|
696
|
+
|
|
673
697
|
eflips.globalConstants["depot"]["vehicle_count"][depot_id][
|
|
674
698
|
vehicle_type
|
|
675
699
|
] = (vehicle_count * 2)
|
|
@@ -775,443 +799,562 @@ def add_evaluation_to_database(
|
|
|
775
799
|
:param scenario: A :class:`eflips.model.Scenario` object containing the input data for the simulation.
|
|
776
800
|
|
|
777
801
|
:param depot_evaluations: A dictionary of :class:`eflips.depot.evaluation.DepotEvaluation` objects. The keys are
|
|
778
|
-
|
|
802
|
+
the depot IDs, as strings.
|
|
779
803
|
|
|
780
804
|
:param session: a SQLAlchemy session object. This is used to add all the simulation results to the
|
|
781
|
-
|
|
805
|
+
database.
|
|
782
806
|
|
|
783
807
|
:return: Nothing. The results are added to the database.
|
|
784
808
|
"""
|
|
785
809
|
|
|
786
810
|
# Read simulation start time
|
|
787
811
|
|
|
788
|
-
for depot_evaluation in depot_evaluations.
|
|
812
|
+
for depot_id, depot_evaluation in depot_evaluations.items():
|
|
789
813
|
simulation_start_time = depot_evaluation.sim_start_datetime
|
|
790
814
|
|
|
791
|
-
#
|
|
815
|
+
# Depot-layer operations
|
|
792
816
|
|
|
793
817
|
list_of_assigned_schedules = []
|
|
794
818
|
|
|
795
|
-
|
|
819
|
+
waiting_area_id = None
|
|
820
|
+
|
|
821
|
+
total_areas = scenario.areas
|
|
822
|
+
for area in total_areas:
|
|
823
|
+
if area.depot_id == int(depot_id) and len(area.processes) == 0:
|
|
824
|
+
waiting_area_id = area.id
|
|
825
|
+
|
|
826
|
+
assert isinstance(waiting_area_id, int) and waiting_area_id > 0, (
|
|
827
|
+
f"Waiting area id should be an integer greater than 0. For every depot there must be at least "
|
|
828
|
+
f"one waiting area."
|
|
829
|
+
)
|
|
830
|
+
|
|
796
831
|
for current_vehicle in depot_evaluation.vehicle_generator.items:
|
|
832
|
+
# Vehicle-layer operations
|
|
833
|
+
|
|
797
834
|
vehicle_type_id = int(current_vehicle.vehicle_type.ID)
|
|
798
835
|
|
|
799
|
-
# Create a Vehicle object for database
|
|
800
836
|
current_vehicle_db = Vehicle(
|
|
801
837
|
vehicle_type_id=vehicle_type_id,
|
|
802
838
|
scenario=scenario,
|
|
803
839
|
name=current_vehicle.ID,
|
|
804
840
|
name_short=None,
|
|
805
841
|
)
|
|
842
|
+
|
|
806
843
|
# Flush the vehicle object to get the vehicle id
|
|
807
844
|
session.add(current_vehicle_db)
|
|
808
845
|
session.flush()
|
|
809
846
|
|
|
810
|
-
dict_of_events =
|
|
847
|
+
dict_of_events = OrderedDict()
|
|
848
|
+
|
|
849
|
+
(
|
|
850
|
+
schedule_current_vehicle,
|
|
851
|
+
earliest_time,
|
|
852
|
+
latest_time,
|
|
853
|
+
# Earliest and latest time defines a time window, only the events within this time window will be
|
|
854
|
+
# handled. It is usually the departure time of the last copy trip in the "early-shifted" copy
|
|
855
|
+
# schedules and the departure time of the first copy trip in the "late-shifted" copy schedules.
|
|
856
|
+
) = _get_finished_schedules_per_vehicle(
|
|
857
|
+
dict_of_events, current_vehicle.finished_trips, current_vehicle_db.id
|
|
858
|
+
)
|
|
811
859
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
list_of_finished_trips.sort(key=lambda x: x.atd)
|
|
860
|
+
try:
|
|
861
|
+
assert earliest_time is not None and latest_time is not None
|
|
815
862
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
863
|
+
except AssertionError as e:
|
|
864
|
+
warnings.warn(
|
|
865
|
+
f"Vehicle {current_vehicle_db.id} has only copied trips. The profiles of this vehicle "
|
|
866
|
+
f"will not be written into database."
|
|
867
|
+
)
|
|
868
|
+
continue
|
|
819
869
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
"end": current_trip.ata,
|
|
824
|
-
"is_copy": current_trip.is_copy,
|
|
825
|
-
"id": current_trip.ID,
|
|
826
|
-
}
|
|
870
|
+
assert (
|
|
871
|
+
earliest_time < latest_time
|
|
872
|
+
), f"Earliest time {earliest_time} is not less than latest time {latest_time}."
|
|
827
873
|
|
|
828
|
-
|
|
829
|
-
assigned_schedule_id = int(current_trip.ID)
|
|
830
|
-
list_of_assigned_schedules.append(
|
|
831
|
-
(assigned_schedule_id, current_vehicle_db.id)
|
|
832
|
-
)
|
|
833
|
-
# Also add two copy trips before and after the non-copy trip as "time boarders" for the depot process
|
|
834
|
-
try:
|
|
835
|
-
if list_of_finished_trips[i + 1].is_copy is True:
|
|
836
|
-
dict_of_events[list_of_finished_trips[i + 1].atd] = {
|
|
837
|
-
"type": "Trip",
|
|
838
|
-
"end": list_of_finished_trips[i + 1].ata,
|
|
839
|
-
"is_copy": list_of_finished_trips[i + 1].is_copy,
|
|
840
|
-
"id": list_of_finished_trips[i + 1].ID,
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
if list_of_finished_trips[i - 1].is_copy is True:
|
|
844
|
-
dict_of_events[list_of_finished_trips[i - 1].atd] = {
|
|
845
|
-
"type": "Trip",
|
|
846
|
-
"end": list_of_finished_trips[i - 1].ata,
|
|
847
|
-
"is_copy": list_of_finished_trips[i - 1].is_copy,
|
|
848
|
-
"id": list_of_finished_trips[i - 1].ID,
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
except IndexError:
|
|
852
|
-
# In case there are no copy trips before or after the non-copy trip
|
|
853
|
-
continue
|
|
854
|
-
|
|
855
|
-
# The range of time of events to be generated. It is between the copy trip before the first non-copy trip
|
|
856
|
-
# and the copy trip after the last non-copy trip
|
|
857
|
-
earliest_time = sorted(dict_of_events.keys())[0]
|
|
858
|
-
latest_time = sorted(dict_of_events.keys())[-1]
|
|
859
|
-
|
|
860
|
-
last_standby_departure_start = 0
|
|
861
|
-
|
|
862
|
-
# For convenience
|
|
863
|
-
area_log = current_vehicle.logger.loggedData["dwd.current_area"]
|
|
864
|
-
slot_log = current_vehicle.logger.loggedData["dwd.current_slot"]
|
|
865
|
-
|
|
866
|
-
waiting_log = current_vehicle.logger.loggedData["area_waiting_time"]
|
|
867
|
-
battery_log = current_vehicle.battery_logs
|
|
868
|
-
|
|
869
|
-
# Create standby events according to waiting_log
|
|
870
|
-
waiting_log_timekeys = sorted(waiting_log.keys())
|
|
871
|
-
|
|
872
|
-
for idx in range(len(waiting_log_timekeys)):
|
|
873
|
-
end_time = waiting_log_timekeys[idx]
|
|
874
|
-
|
|
875
|
-
# Only extract events if the time is within the upper mentioned range
|
|
876
|
-
|
|
877
|
-
if earliest_time < end_time < latest_time:
|
|
878
|
-
waiting_info = waiting_log[end_time]
|
|
879
|
-
|
|
880
|
-
if waiting_info["waiting_time"] == 0:
|
|
881
|
-
continue
|
|
882
|
-
|
|
883
|
-
# Vehicle is waiting in the last area in waiting_log and expecting to enter the current area
|
|
884
|
-
expected_area = waiting_info["area"]
|
|
885
|
-
# Find the area for standby arrival event
|
|
886
|
-
|
|
887
|
-
# Get the corresponding depot id first by grabbing one of the rotations and get its departure station
|
|
888
|
-
some_rotation_id = current_vehicle.finished_trips[0].ID
|
|
889
|
-
some_rotation = (
|
|
890
|
-
session.query(Rotation)
|
|
891
|
-
.filter(Rotation.id == some_rotation_id)
|
|
892
|
-
.one()
|
|
893
|
-
)
|
|
894
|
-
start_station = some_rotation.trips[0].route.departure_station_id
|
|
895
|
-
depot_id = (
|
|
896
|
-
session.query(Depot.id)
|
|
897
|
-
.join(Station)
|
|
898
|
-
.filter(Station.id == start_station)
|
|
899
|
-
.one()[0]
|
|
900
|
-
)
|
|
874
|
+
list_of_assigned_schedules.extend(schedule_current_vehicle)
|
|
901
875
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
Process.duration.is_(None),
|
|
910
|
-
Process.electric_power.is_(None),
|
|
911
|
-
Area.vehicle_type_id
|
|
912
|
-
== int(current_vehicle.vehicle_type.ID),
|
|
913
|
-
Area.scenario_id == scenario.id,
|
|
914
|
-
Area.depot_id == depot_id,
|
|
915
|
-
)
|
|
916
|
-
.one()[0]
|
|
917
|
-
)
|
|
876
|
+
_generate_vehicle_events(
|
|
877
|
+
dict_of_events,
|
|
878
|
+
current_vehicle,
|
|
879
|
+
waiting_area_id,
|
|
880
|
+
earliest_time,
|
|
881
|
+
latest_time,
|
|
882
|
+
)
|
|
918
883
|
|
|
919
|
-
|
|
884
|
+
# Python passes dictionaries by reference
|
|
920
885
|
|
|
921
|
-
|
|
886
|
+
_complete_standby_departure_events(dict_of_events, latest_time)
|
|
922
887
|
|
|
923
|
-
|
|
888
|
+
_add_soc_to_events(dict_of_events, current_vehicle.battery_logs)
|
|
924
889
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
890
|
+
try:
|
|
891
|
+
assert (not dict_of_events) is False
|
|
892
|
+
except AssertionError as e:
|
|
893
|
+
warnings.warn(
|
|
894
|
+
f"Vehicle {current_vehicle_db.id} has no valid events. The vehicle will not be written "
|
|
895
|
+
f"into database."
|
|
896
|
+
)
|
|
928
897
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
898
|
+
continue
|
|
899
|
+
|
|
900
|
+
_add_events_into_database(
|
|
901
|
+
current_vehicle_db,
|
|
902
|
+
dict_of_events,
|
|
903
|
+
session,
|
|
904
|
+
scenario,
|
|
905
|
+
simulation_start_time,
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
# Postprocessing of events
|
|
909
|
+
_update_vehicle_in_rotation(session, scenario, list_of_assigned_schedules)
|
|
910
|
+
_update_waiting_events(session, scenario, waiting_area_id)
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def _get_finished_schedules_per_vehicle(
|
|
914
|
+
dict_of_events, list_of_finished_trips: List, db_vehicle_id: int
|
|
915
|
+
):
|
|
916
|
+
"""
|
|
917
|
+
This function completes the following tasks:
|
|
918
|
+
1. It gets the finished non-copy schedules of the current vehicle,
|
|
919
|
+
which will be used in :func:`_update_vehicle_in_rotation()`.
|
|
920
|
+
|
|
921
|
+
2. It fills the dictionary of events with the trip_ids of the current vehicle.
|
|
922
|
+
|
|
923
|
+
3. It returns an earliest and a latest time according to this vehicle's schedules. Only processes happening within
|
|
924
|
+
this time window will be handled later.
|
|
936
925
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
926
|
+
Usually the earliest time is the departure time of the last copy trip in the "early-shifted" copy schedules
|
|
927
|
+
and the lastest time is the departure time of the first copy trip in the "late-shifted" copy schedules.
|
|
928
|
+
|
|
929
|
+
# If the vehicle's first trip is a non-copy trip, the earliest time is the departure time of the first trip. If the
|
|
930
|
+
# vehicle's last trip is a non-copy trip, the latest time is the departure time of the last trip.
|
|
931
|
+
|
|
932
|
+
:param dict_of_events: An ordered dictionary storing the data related to an event. The keys are the start times of
|
|
933
|
+
the events.
|
|
934
|
+
:param list_of_finished_trips: A list of finished trips of a vehicle directly from
|
|
935
|
+
:class:`eflips.depot.simple_vehicle.SimpleVehicle` object.
|
|
936
|
+
|
|
937
|
+
:param db_vehicle_id: The vehicle id in the database.
|
|
938
|
+
|
|
939
|
+
:return: A tuple of three elements. The first element is a list of finished schedules of the vehicle. The second and
|
|
940
|
+
third elements are the earliest and latest time of the vehicle's schedules.
|
|
941
|
+
"""
|
|
942
|
+
finished_schedules = []
|
|
943
|
+
|
|
944
|
+
list_of_finished_trips.sort(key=lambda x: x.atd)
|
|
945
|
+
earliest_time = None
|
|
946
|
+
latest_time = None
|
|
947
|
+
|
|
948
|
+
for i in range(len(list_of_finished_trips)):
|
|
949
|
+
assert list_of_finished_trips[i].atd == list_of_finished_trips[i].std, (
|
|
950
|
+
"The trip {current_trip.ID} is delayed. The simulation doesn't "
|
|
951
|
+
"support delayed trips for now."
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
if list_of_finished_trips[i].is_copy is False:
|
|
955
|
+
current_trip = list_of_finished_trips[i]
|
|
956
|
+
|
|
957
|
+
finished_schedules.append((int(current_trip.ID), db_vehicle_id))
|
|
958
|
+
dict_of_events[current_trip.atd] = {
|
|
959
|
+
"type": "Trip",
|
|
960
|
+
"id": int(current_trip.ID),
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if i == 0:
|
|
964
|
+
earliest_time = current_trip.atd
|
|
965
|
+
|
|
966
|
+
if i == len(list_of_finished_trips) - 1:
|
|
967
|
+
latest_time = current_trip.atd
|
|
968
|
+
|
|
969
|
+
if i != 0 and list_of_finished_trips[i - 1].is_copy is True:
|
|
970
|
+
earliest_time = list_of_finished_trips[i - 1].atd
|
|
971
|
+
|
|
972
|
+
if (
|
|
973
|
+
i != len(list_of_finished_trips) - 1
|
|
974
|
+
or list_of_finished_trips[i + 1].is_copy is True
|
|
975
|
+
):
|
|
976
|
+
latest_time = list_of_finished_trips[i + 1].atd
|
|
977
|
+
|
|
978
|
+
return finished_schedules, earliest_time, latest_time
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def _generate_vehicle_events(
|
|
982
|
+
dict_of_events,
|
|
983
|
+
current_vehicle: SimpleVehicle,
|
|
984
|
+
virtual_waiting_area_id: int,
|
|
985
|
+
earliest_time: datetime.datetime,
|
|
986
|
+
latest_time: datetime.datetime,
|
|
987
|
+
) -> None:
|
|
988
|
+
"""
|
|
989
|
+
This function generates and ordered dictionary storing the data related to an event. It returns a dictionary. The keys are the start times of the
|
|
990
|
+
events. The values are also dictionaries containing:
|
|
991
|
+
- type: The type of the event.
|
|
992
|
+
- end: The end time of the event.
|
|
993
|
+
- area: The area id of the event.
|
|
994
|
+
- slot: The slot id of the event.
|
|
995
|
+
- id: The id of the event-related process.
|
|
996
|
+
|
|
997
|
+
For trips, only the type is stored.
|
|
998
|
+
|
|
999
|
+
For waiting events, the slot is not stored for now.
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
:param current_vehicle: a :class:`eflips.depot.simple_vehicle.SimpleVehicle` object.
|
|
1003
|
+
|
|
1004
|
+
:param virtual_waiting_area_id: the id of the virtual waiting area. Vehicles waiting for the first process will park here.
|
|
1005
|
+
|
|
1006
|
+
:param earliest_time: the earliest relevant time of the current vehicle. Any events earlier than this will not be
|
|
1007
|
+
handled.
|
|
1008
|
+
|
|
1009
|
+
:param latest_time: the latest relevant time of the current vehicle. Any events later than this will not be handled.
|
|
1010
|
+
|
|
1011
|
+
:return: None. The results are added to the dictionary.
|
|
1012
|
+
"""
|
|
1013
|
+
|
|
1014
|
+
# For convenience
|
|
1015
|
+
area_log = current_vehicle.logger.loggedData["dwd.current_area"]
|
|
1016
|
+
slot_log = current_vehicle.logger.loggedData["dwd.current_slot"]
|
|
1017
|
+
waiting_log = current_vehicle.logger.loggedData["area_waiting_time"]
|
|
1018
|
+
|
|
1019
|
+
# Handling waiting events
|
|
1020
|
+
waiting_log_timekeys = sorted(waiting_log.keys())
|
|
1021
|
+
|
|
1022
|
+
for idx in range(len(waiting_log_timekeys)):
|
|
1023
|
+
waiting_end_time = waiting_log_timekeys[idx]
|
|
1024
|
+
|
|
1025
|
+
# Only extract events if the time is within the upper mentioned range
|
|
1026
|
+
|
|
1027
|
+
if earliest_time <= waiting_end_time <= latest_time:
|
|
1028
|
+
waiting_info = waiting_log[waiting_end_time]
|
|
1029
|
+
|
|
1030
|
+
if waiting_info["waiting_time"] == 0:
|
|
1031
|
+
continue
|
|
1032
|
+
|
|
1033
|
+
warnings.warn(
|
|
1034
|
+
f"Vehicle {current_vehicle.ID} has been waiting for {waiting_info['waiting_time']} seconds. "
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
start_time = waiting_end_time - waiting_info["waiting_time"]
|
|
1038
|
+
|
|
1039
|
+
if waiting_info["area"] == waiting_log[waiting_log_timekeys[0]]["area"]:
|
|
1040
|
+
# if the vehicle is waiting for the first process, put it in the virtual waiting area
|
|
1041
|
+
waiting_area_id = virtual_waiting_area_id
|
|
1042
|
+
else:
|
|
1043
|
+
# If the vehicle is waiting for other processes,
|
|
1044
|
+
# put it in the area of the prodecessor process of the waited process.
|
|
1045
|
+
waiting_area_id = waiting_log[waiting_log_timekeys[idx - 1]]["area"]
|
|
1046
|
+
|
|
1047
|
+
dict_of_events[start_time] = {
|
|
1048
|
+
"type": "Standby",
|
|
1049
|
+
"end": waiting_end_time,
|
|
1050
|
+
"area": waiting_area_id,
|
|
1051
|
+
"is_waiting": True,
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
# Create a list of battery log in order of time asc. Convenient for looking up corresponding soc
|
|
1055
|
+
|
|
1056
|
+
for time_stamp, process_log in current_vehicle.logger.loggedData[
|
|
1057
|
+
"dwd.active_processes_copy"
|
|
1058
|
+
].items():
|
|
1059
|
+
if earliest_time <= time_stamp <= latest_time:
|
|
1060
|
+
num_process = len(process_log)
|
|
1061
|
+
if num_process == 0:
|
|
1062
|
+
# A departure happens and this trip should already be stored in the dictionary
|
|
1063
|
+
pass
|
|
1064
|
+
else:
|
|
1065
|
+
for process in process_log:
|
|
1066
|
+
current_area = area_log[time_stamp]
|
|
1067
|
+
current_slot = slot_log[time_stamp]
|
|
1068
|
+
|
|
1069
|
+
if current_area is None or current_slot is None:
|
|
1070
|
+
raise ValueError(
|
|
1071
|
+
f"For process {process.ID} Area and slot should not be None."
|
|
1072
|
+
)
|
|
1073
|
+
|
|
1074
|
+
match process.status:
|
|
1075
|
+
case ProcessStatus.COMPLETED | ProcessStatus.CANCELLED:
|
|
1076
|
+
assert (
|
|
1077
|
+
len(process.starts) == 1 and len(process.ends) == 1
|
|
1078
|
+
), (
|
|
1079
|
+
f"Current process {process.ID} is completed and should only contain one start and "
|
|
1080
|
+
f"one end time."
|
|
1081
|
+
)
|
|
1082
|
+
|
|
1083
|
+
if process.dur > 0:
|
|
1084
|
+
# Valid duration
|
|
1085
|
+
dict_of_events[time_stamp] = {
|
|
1086
|
+
"type": type(process).__name__,
|
|
1087
|
+
"end": process.ends[0],
|
|
1088
|
+
"area": current_area.ID,
|
|
1089
|
+
"slot": current_slot,
|
|
1090
|
+
"id": process.ID,
|
|
1091
|
+
}
|
|
1092
|
+
else:
|
|
1093
|
+
# Duration is 0
|
|
1094
|
+
assert current_area.issink is True, (
|
|
1095
|
+
f"A process with no duration could only "
|
|
1096
|
+
f"happen in the last area before dispatched"
|
|
1097
|
+
)
|
|
1098
|
+
if (
|
|
1099
|
+
time_stamp in dict_of_events.keys()
|
|
1100
|
+
and "end" in dict_of_events[time_stamp].keys()
|
|
1101
|
+
):
|
|
1102
|
+
start_this_event = dict_of_events[time_stamp]["end"]
|
|
1103
|
+
dict_of_events[start_this_event] = {
|
|
1104
|
+
"type": type(process).__name__,
|
|
1105
|
+
"area": current_area.ID,
|
|
1106
|
+
"slot": current_slot,
|
|
1107
|
+
"id": process.ID,
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
case ProcessStatus.IN_PROGRESS:
|
|
1111
|
+
assert (
|
|
1112
|
+
len(process.starts) == 1 and len(process.ends) == 0
|
|
1113
|
+
), f"Current process {process.ID} is marked IN_PROGRESS, but has an end."
|
|
1114
|
+
|
|
1115
|
+
if current_area is None or current_slot is None:
|
|
1116
|
+
raise ValueError(
|
|
1117
|
+
f"For process {process.ID} Area and slot should not be None."
|
|
1118
|
+
)
|
|
1119
|
+
|
|
1120
|
+
if process.dur > 0:
|
|
1121
|
+
# Valid duration
|
|
1122
|
+
dict_of_events[time_stamp] = {
|
|
1123
|
+
"type": type(process).__name__,
|
|
1124
|
+
"end": process.etc,
|
|
1125
|
+
"area": current_area.ID,
|
|
1126
|
+
"slot": current_slot,
|
|
1127
|
+
"id": process.ID,
|
|
1128
|
+
}
|
|
1100
1129
|
else:
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1130
|
+
raise NotImplementedError(
|
|
1131
|
+
"We believe this should never happen. If it happens, handle it here."
|
|
1132
|
+
)
|
|
1133
|
+
|
|
1134
|
+
# The following ProcessStatus possibly only happen while the simulation is running,
|
|
1135
|
+
# not in the results
|
|
1136
|
+
case ProcessStatus.WAITING:
|
|
1137
|
+
raise NotImplementedError(
|
|
1138
|
+
f"Current process {process.ID} is waiting. Not implemented yet."
|
|
1139
|
+
)
|
|
1140
|
+
|
|
1141
|
+
case ProcessStatus.NOT_STARTED:
|
|
1142
|
+
raise NotImplementedError(
|
|
1143
|
+
f"Current process {process.ID} is not started. Not implemented yet."
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1106
1146
|
case _:
|
|
1107
1147
|
raise ValueError(
|
|
1108
|
-
|
|
1109
|
-
'"Standby", "Precondition"'
|
|
1148
|
+
f"Invalid process status {process.status} for process {process.ID}."
|
|
1110
1149
|
)
|
|
1111
1150
|
|
|
1112
|
-
# End time of 0-duration processes are start time of the next process
|
|
1113
|
-
|
|
1114
|
-
if "end" not in process_dict:
|
|
1115
|
-
# End time will be the one time key "later"
|
|
1116
|
-
end_time = time_keys[time_keys.index(start_time) + 1]
|
|
1117
|
-
|
|
1118
|
-
process_dict["end"] = end_time
|
|
1119
|
-
|
|
1120
|
-
# Get soc
|
|
1121
|
-
soc_start = None
|
|
1122
|
-
soc_end = None
|
|
1123
|
-
|
|
1124
|
-
for i in range(len(battery_log_list)):
|
|
1125
|
-
# Access the correct battery log according to time since there is only one battery log for each time
|
|
1126
|
-
log = battery_log_list[i]
|
|
1127
|
-
|
|
1128
|
-
if log[0] == start_time:
|
|
1129
|
-
soc_start = log[1]
|
|
1130
|
-
if log[0] == process_dict["end"]:
|
|
1131
|
-
soc_end = log[1]
|
|
1132
|
-
if log[0] < start_time < battery_log_list[i + 1][0]:
|
|
1133
|
-
soc_start = log[1]
|
|
1134
|
-
if log[0] < process_dict["end"] < battery_log_list[i + 1][0]:
|
|
1135
|
-
soc_end = log[1]
|
|
1136
|
-
|
|
1137
|
-
current_event = Event(
|
|
1138
|
-
scenario=scenario,
|
|
1139
|
-
vehicle_type_id=vehicle_type_id,
|
|
1140
|
-
vehicle=current_vehicle_db,
|
|
1141
|
-
station_id=None,
|
|
1142
|
-
area_id=int(process_dict["area"]),
|
|
1143
|
-
subloc_no=int(process_dict["slot"]),
|
|
1144
|
-
trip_id=None,
|
|
1145
|
-
time_start=timedelta(seconds=start_time)
|
|
1146
|
-
+ simulation_start_time,
|
|
1147
|
-
time_end=timedelta(seconds=process_dict["end"])
|
|
1148
|
-
+ simulation_start_time,
|
|
1149
|
-
soc_start=soc_start if soc_start is not None else soc_end,
|
|
1150
|
-
soc_end=soc_end
|
|
1151
|
-
if soc_end is not None
|
|
1152
|
-
else soc_start, # if only one battery log is found,
|
|
1153
|
-
# then this is not an event with soc change
|
|
1154
|
-
event_type=event_type,
|
|
1155
|
-
description=None,
|
|
1156
|
-
timeseries=None,
|
|
1157
|
-
)
|
|
1158
1151
|
|
|
1159
|
-
|
|
1152
|
+
def _complete_standby_departure_events(
|
|
1153
|
+
dict_of_events: Dict, latest_time: datetime.datetime
|
|
1154
|
+
) -> None:
|
|
1155
|
+
"""
|
|
1156
|
+
This function completes the standby departure events by adding an end time to each standby departure event.
|
|
1157
|
+
|
|
1158
|
+
:param dict_of_events: a dictionary containing the events of a vehicle. The keys are the start times of the events.
|
|
1159
|
+
|
|
1160
|
+
:param latest_time: the latest relevant time of the current vehicle. Any events later than this will not be handled.
|
|
1161
|
+
|
|
1162
|
+
:return: None. The results are added to the dictionary.
|
|
1163
|
+
"""
|
|
1164
|
+
for i in range(len(dict_of_events.keys())):
|
|
1165
|
+
time_keys = sorted(dict_of_events.keys())
|
|
1166
|
+
|
|
1167
|
+
process_dict = dict_of_events[time_keys[i]]
|
|
1168
|
+
if "end" not in process_dict and process_dict["type"] != "Trip":
|
|
1169
|
+
# End time of a standby_departure will be the start of the following trip
|
|
1170
|
+
if i == len(time_keys) - 1:
|
|
1171
|
+
# The event reaches simulation end
|
|
1172
|
+
end_time = latest_time
|
|
1173
|
+
else:
|
|
1174
|
+
end_time = time_keys[i + 1]
|
|
1175
|
+
|
|
1176
|
+
process_dict["end"] = end_time
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
def _add_soc_to_events(dict_of_events, battery_log) -> None:
|
|
1180
|
+
"""
|
|
1181
|
+
This function completes the soc of each event by looking up the battery log.
|
|
1182
|
+
|
|
1183
|
+
:param dict_of_events: a dictionary containing the events of a vehicle. The keys are the start times of the events.
|
|
1184
|
+
|
|
1185
|
+
:param battery_log: a list of battery logs of a vehicle.
|
|
1186
|
+
|
|
1187
|
+
:return: None. The results are added to the dictionary.
|
|
1188
|
+
"""
|
|
1189
|
+
battery_log_list = []
|
|
1190
|
+
for log in battery_log:
|
|
1191
|
+
battery_log_list.append((log.t, log.energy / log.energy_real))
|
|
1192
|
+
|
|
1193
|
+
time_keys = sorted(dict_of_events.keys())
|
|
1194
|
+
for i in range(len(time_keys)):
|
|
1195
|
+
# Get soc
|
|
1196
|
+
soc_start = None
|
|
1197
|
+
soc_end = None
|
|
1198
|
+
start_time = time_keys[i]
|
|
1199
|
+
process_dict = dict_of_events[time_keys[i]]
|
|
1200
|
+
for j in range(len(battery_log_list)):
|
|
1201
|
+
# Access the correct battery log according to time since there is only one battery log for each time
|
|
1202
|
+
log = battery_log_list[j]
|
|
1203
|
+
|
|
1204
|
+
if process_dict["type"] != "Trip":
|
|
1205
|
+
if log[0] == start_time:
|
|
1206
|
+
soc_start = log[1]
|
|
1207
|
+
if log[0] == process_dict["end"]:
|
|
1208
|
+
soc_end = log[1]
|
|
1209
|
+
if log[0] < start_time < battery_log_list[j + 1][0]:
|
|
1210
|
+
soc_start = log[1]
|
|
1211
|
+
if log[0] < process_dict["end"] < battery_log_list[j + 1][0]:
|
|
1212
|
+
soc_end = log[1]
|
|
1213
|
+
|
|
1214
|
+
process_dict["soc_start"] = soc_start
|
|
1215
|
+
process_dict["soc_end"] = soc_end
|
|
1216
|
+
|
|
1217
|
+
else:
|
|
1218
|
+
continue
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
def _add_events_into_database(
|
|
1222
|
+
db_vehicle, dict_of_events, session, scenario, simulation_start_time
|
|
1223
|
+
) -> None:
|
|
1224
|
+
"""
|
|
1225
|
+
This function generates :class:`eflips.model.Event` objects from the dictionary of events and adds them into the
|
|
1226
|
+
database.
|
|
1227
|
+
|
|
1228
|
+
:param db_vehicle: vehicle object in the database
|
|
1229
|
+
|
|
1230
|
+
:param dict_of_events: dictionary containing the events of a vehicle. The keys are the start times of the events.
|
|
1231
|
+
|
|
1232
|
+
:param session: a :class:`sqlalchemy.orm.Session` object for database connection.
|
|
1233
|
+
|
|
1234
|
+
:param scenario: the current simulated scenario
|
|
1160
1235
|
|
|
1161
|
-
|
|
1236
|
+
:param simulation_start_time: simulation start time in :class:`datetime.datetime` format
|
|
1237
|
+
|
|
1238
|
+
:return: None. The results are added to the database.
|
|
1239
|
+
"""
|
|
1240
|
+
for start_time, process_dict in dict_of_events.items():
|
|
1241
|
+
# Generate EventType
|
|
1242
|
+
match process_dict["type"]:
|
|
1243
|
+
case "Serve":
|
|
1244
|
+
event_type = EventType.SERVICE
|
|
1245
|
+
case "Charge":
|
|
1246
|
+
event_type = EventType.CHARGING_DEPOT
|
|
1247
|
+
case "Standby":
|
|
1162
1248
|
if (
|
|
1163
|
-
|
|
1164
|
-
and
|
|
1249
|
+
"is_waiting" in process_dict.keys()
|
|
1250
|
+
and process_dict["is_waiting"] is True
|
|
1165
1251
|
):
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1252
|
+
event_type = EventType.STANDBY
|
|
1253
|
+
else:
|
|
1254
|
+
event_type = EventType.STANDBY_DEPARTURE
|
|
1255
|
+
case "Precondition":
|
|
1256
|
+
event_type = EventType.PRECONDITIONING
|
|
1257
|
+
case "Trip":
|
|
1258
|
+
continue
|
|
1259
|
+
case _:
|
|
1260
|
+
raise ValueError(
|
|
1261
|
+
'Invalid process type %s. Valid process types are "Serve", "Charge", '
|
|
1262
|
+
'"Standby", "Precondition"'
|
|
1263
|
+
)
|
|
1174
1264
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1265
|
+
current_event = Event(
|
|
1266
|
+
scenario=scenario,
|
|
1267
|
+
vehicle_type_id=db_vehicle.vehicle_type_id,
|
|
1268
|
+
vehicle=db_vehicle,
|
|
1269
|
+
station_id=None,
|
|
1270
|
+
area_id=int(process_dict["area"]),
|
|
1271
|
+
subloc_no=int(process_dict["slot"])
|
|
1272
|
+
if "slot" in process_dict.keys()
|
|
1273
|
+
else 00,
|
|
1274
|
+
trip_id=None,
|
|
1275
|
+
time_start=timedelta(seconds=start_time) + simulation_start_time,
|
|
1276
|
+
time_end=timedelta(seconds=process_dict["end"]) + simulation_start_time,
|
|
1277
|
+
soc_start=process_dict["soc_start"]
|
|
1278
|
+
if process_dict["soc_start"] is not None
|
|
1279
|
+
else process_dict["soc_end"],
|
|
1280
|
+
soc_end=process_dict["soc_end"]
|
|
1281
|
+
if process_dict["soc_end"] is not None
|
|
1282
|
+
else process_dict["soc_start"], # if only one battery log is found,
|
|
1283
|
+
# then this is not an event with soc change
|
|
1284
|
+
event_type=event_type,
|
|
1285
|
+
description=process_dict["id"] if "id" in process_dict.keys() else None,
|
|
1286
|
+
timeseries=None,
|
|
1287
|
+
)
|
|
1181
1288
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1289
|
+
session.add(current_event)
|
|
1290
|
+
|
|
1291
|
+
# For non-copy schedules with no predecessor events, adding a dummy standby-departure
|
|
1292
|
+
|
|
1293
|
+
time_keys = sorted(dict_of_events.keys())
|
|
1294
|
+
if (
|
|
1295
|
+
dict_of_events[time_keys[0]]["type"]
|
|
1296
|
+
== "Trip"
|
|
1297
|
+
# and dict_of_events[time_keys[0]]["is_copy"] is False
|
|
1298
|
+
):
|
|
1299
|
+
standby_start = time_keys[0] - 1
|
|
1300
|
+
standby_end = time_keys[0]
|
|
1301
|
+
rotation_id = int(dict_of_events[time_keys[0]]["id"])
|
|
1302
|
+
area = (
|
|
1303
|
+
session.query(Area)
|
|
1304
|
+
.filter(Area.vehicle_type_id == db_vehicle.vehicle_type_id)
|
|
1305
|
+
.first()
|
|
1306
|
+
)
|
|
1188
1307
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
subloc_no=area.capacity,
|
|
1196
|
-
trip_id=None,
|
|
1197
|
-
time_start=timedelta(seconds=standby_start)
|
|
1198
|
-
+ simulation_start_time,
|
|
1199
|
-
time_end=timedelta(seconds=standby_end) + simulation_start_time,
|
|
1200
|
-
soc_start=soc,
|
|
1201
|
-
soc_end=soc,
|
|
1202
|
-
event_type=EventType.STANDBY_DEPARTURE,
|
|
1203
|
-
description=f"DUMMY Standby event for {rotation_id}.",
|
|
1204
|
-
timeseries=None,
|
|
1205
|
-
)
|
|
1308
|
+
first_trip = (
|
|
1309
|
+
session.query(Trip)
|
|
1310
|
+
.filter(Trip.rotation_id == rotation_id)
|
|
1311
|
+
.order_by(Trip.departure_time)
|
|
1312
|
+
.first()
|
|
1313
|
+
)
|
|
1206
1314
|
|
|
1207
|
-
|
|
1315
|
+
soc = (
|
|
1316
|
+
session.query(Event.soc_end)
|
|
1317
|
+
.filter(Event.scenario == scenario)
|
|
1318
|
+
.filter(Event.trip_id == first_trip.id)
|
|
1319
|
+
.first()[0]
|
|
1320
|
+
)
|
|
1208
1321
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1322
|
+
standby_event = Event(
|
|
1323
|
+
scenario=scenario,
|
|
1324
|
+
vehicle_type_id=db_vehicle.vehicle_type_id,
|
|
1325
|
+
vehicle=db_vehicle,
|
|
1326
|
+
station_id=None,
|
|
1327
|
+
area_id=area.id,
|
|
1328
|
+
subloc_no=area.capacity,
|
|
1329
|
+
trip_id=None,
|
|
1330
|
+
time_start=timedelta(seconds=standby_start) + simulation_start_time,
|
|
1331
|
+
time_end=timedelta(seconds=standby_end) + simulation_start_time,
|
|
1332
|
+
soc_start=soc,
|
|
1333
|
+
soc_end=soc,
|
|
1334
|
+
event_type=EventType.STANDBY_DEPARTURE,
|
|
1335
|
+
description=f"DUMMY Standby event for {rotation_id}.",
|
|
1336
|
+
timeseries=None,
|
|
1337
|
+
)
|
|
1338
|
+
|
|
1339
|
+
session.add(standby_event)
|
|
1340
|
+
|
|
1341
|
+
session.flush()
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def _update_vehicle_in_rotation(session, scenario, list_of_assigned_schedules) -> None:
|
|
1345
|
+
"""
|
|
1346
|
+
This function updates the vehicle id assigned to the rotations and deletes the events that are not depot events.
|
|
1347
|
+
:param session: a :class:`sqlalchemy.orm.Session` object for database connection.
|
|
1348
|
+
:param scenario: the current simulated scenario
|
|
1349
|
+
:param list_of_assigned_schedules: a list of tuples containing the rotation id and the vehicle id.
|
|
1350
|
+
:return: None. The results are added to the database.
|
|
1351
|
+
"""
|
|
1352
|
+
# New rotation assignment
|
|
1353
|
+
for schedule_id, vehicle_id in list_of_assigned_schedules:
|
|
1354
|
+
# Get corresponding old vehicle id
|
|
1355
|
+
session.query(Rotation).filter(Rotation.id == schedule_id).update(
|
|
1356
|
+
{"vehicle_id": vehicle_id}, synchronize_session="auto"
|
|
1357
|
+
)
|
|
1215
1358
|
|
|
1216
1359
|
# Delete all non-depot events
|
|
1217
1360
|
session.query(Event).filter(
|
|
@@ -1234,3 +1377,122 @@ def add_evaluation_to_database(
|
|
|
1234
1377
|
).delete()
|
|
1235
1378
|
|
|
1236
1379
|
session.flush()
|
|
1380
|
+
|
|
1381
|
+
|
|
1382
|
+
def _update_waiting_events(session, scenario, waiting_area_id) -> None:
|
|
1383
|
+
"""
|
|
1384
|
+
This function evaluates the capacity of waiting area and assigns the waiting events to corresponding slots in the
|
|
1385
|
+
waiting area.
|
|
1386
|
+
|
|
1387
|
+
:param session: a :class:`sqlalchemy.orm.Session` object for database connection.
|
|
1388
|
+
|
|
1389
|
+
:param scenario: the current simulated scenario.
|
|
1390
|
+
|
|
1391
|
+
:param waiting_area_id: id of the waiting area.
|
|
1392
|
+
|
|
1393
|
+
:raise ValueError: if the waiting area capacity is less than the peak waiting occupancy.
|
|
1394
|
+
|
|
1395
|
+
:return: None. The results are added to the database.
|
|
1396
|
+
"""
|
|
1397
|
+
# Process all the STANDBY (waiting) events #
|
|
1398
|
+
all_waiting_starts = (
|
|
1399
|
+
session.query(Event)
|
|
1400
|
+
.filter(
|
|
1401
|
+
Event.scenario_id == scenario.id,
|
|
1402
|
+
Event.event_type == EventType.STANDBY,
|
|
1403
|
+
Event.area_id == waiting_area_id,
|
|
1404
|
+
)
|
|
1405
|
+
.all()
|
|
1406
|
+
)
|
|
1407
|
+
|
|
1408
|
+
all_waiting_ends = (
|
|
1409
|
+
session.query(Event)
|
|
1410
|
+
.filter(
|
|
1411
|
+
Event.scenario_id == scenario.id,
|
|
1412
|
+
Event.event_type == EventType.STANDBY,
|
|
1413
|
+
Event.area_id == waiting_area_id,
|
|
1414
|
+
)
|
|
1415
|
+
.all()
|
|
1416
|
+
)
|
|
1417
|
+
|
|
1418
|
+
assert len(all_waiting_starts) == len(
|
|
1419
|
+
all_waiting_ends
|
|
1420
|
+
), f"Number of waiting events starts {len(all_waiting_starts)} is not equal to the number of waiting event ends"
|
|
1421
|
+
|
|
1422
|
+
if len(all_waiting_starts) == 0:
|
|
1423
|
+
print(
|
|
1424
|
+
"No waiting events found. The depot has enough capacity for waiting. Change the waiting area capacity to 10 as buffer."
|
|
1425
|
+
)
|
|
1426
|
+
|
|
1427
|
+
session.query(Area).filter(Area.id == waiting_area_id).update(
|
|
1428
|
+
{"capacity": 10}, synchronize_session="auto"
|
|
1429
|
+
)
|
|
1430
|
+
|
|
1431
|
+
return
|
|
1432
|
+
|
|
1433
|
+
list_waiting_timestamps = []
|
|
1434
|
+
for waiting_start in all_waiting_starts:
|
|
1435
|
+
list_waiting_timestamps.append(
|
|
1436
|
+
{"timestamp": waiting_start.time_start, "event": (waiting_start.id, 1)}
|
|
1437
|
+
)
|
|
1438
|
+
|
|
1439
|
+
for waiting_end in all_waiting_ends:
|
|
1440
|
+
list_waiting_timestamps.append(
|
|
1441
|
+
{"timestamp": waiting_end.time_end, "event": (waiting_end.id, -1)}
|
|
1442
|
+
)
|
|
1443
|
+
|
|
1444
|
+
list_waiting_timestamps.sort(key=lambda x: x["timestamp"])
|
|
1445
|
+
start_and_end_records = [wt["event"][1] for wt in list_waiting_timestamps]
|
|
1446
|
+
|
|
1447
|
+
peak_waiting_occupancy = max(list(itertools.accumulate(start_and_end_records)))
|
|
1448
|
+
|
|
1449
|
+
# Assuming that there is only one waiting area in each depot
|
|
1450
|
+
|
|
1451
|
+
waiting_area_id = all_waiting_starts[0].area_id
|
|
1452
|
+
waiting_area = session.query(Area).filter(Area.id == waiting_area_id).first()
|
|
1453
|
+
if waiting_area.capacity > peak_waiting_occupancy:
|
|
1454
|
+
warnings.warn(
|
|
1455
|
+
f"Current waiting area capacity {waiting_area.capacity} "
|
|
1456
|
+
f"is greater than the peak waiting occupancy. Updating the capacity to {peak_waiting_occupancy}."
|
|
1457
|
+
)
|
|
1458
|
+
session.query(Area).filter(Area.id == waiting_area_id).update(
|
|
1459
|
+
{"capacity": peak_waiting_occupancy}, synchronize_session="auto"
|
|
1460
|
+
)
|
|
1461
|
+
session.flush()
|
|
1462
|
+
elif waiting_area.capacity < peak_waiting_occupancy:
|
|
1463
|
+
raise ValueError(
|
|
1464
|
+
f"Waiting area capacity is less than the peak waiting occupancy. "
|
|
1465
|
+
f"Waiting area capacity: {waiting_area.capacity}, peak waiting occupancy: {peak_waiting_occupancy}."
|
|
1466
|
+
)
|
|
1467
|
+
else:
|
|
1468
|
+
pass
|
|
1469
|
+
|
|
1470
|
+
session.flush()
|
|
1471
|
+
|
|
1472
|
+
# Update waiting slots
|
|
1473
|
+
virtual_waiting_area = [None] * peak_waiting_occupancy
|
|
1474
|
+
for wt in list_waiting_timestamps:
|
|
1475
|
+
# check in
|
|
1476
|
+
if wt["event"][1] == 1:
|
|
1477
|
+
for i in range(len(virtual_waiting_area)):
|
|
1478
|
+
if virtual_waiting_area[i] is None:
|
|
1479
|
+
virtual_waiting_area[i] = wt["event"][0]
|
|
1480
|
+
session.query(Event).filter(Event.id == wt["event"][0]).update(
|
|
1481
|
+
{"subloc_no": i}, synchronize_session="auto"
|
|
1482
|
+
)
|
|
1483
|
+
break
|
|
1484
|
+
# check out
|
|
1485
|
+
else:
|
|
1486
|
+
for i in range(len(virtual_waiting_area)):
|
|
1487
|
+
if virtual_waiting_area[i] == wt["event"][0]:
|
|
1488
|
+
current_waiting_event = (
|
|
1489
|
+
session.query(Event).filter(Event.id == wt["event"][0]).first()
|
|
1490
|
+
)
|
|
1491
|
+
assert current_waiting_event.subloc_no == i, (
|
|
1492
|
+
f"Subloc number of the event {current_waiting_event.id} is not equal to the index of the "
|
|
1493
|
+
f"event in the virtual waiting area."
|
|
1494
|
+
)
|
|
1495
|
+
virtual_waiting_area[i] = None
|
|
1496
|
+
break
|
|
1497
|
+
|
|
1498
|
+
session.flush()
|