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.
@@ -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 typing import Any, Dict, Optional, Union
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
- an integer specifying the ID of a scenario in the database. Or any other object that has an attribute
115
- ``id`` that is an integer. If no :class:`eflips.model.Scenario` object is passed, the ``database_url``
116
- parameter must be set to a valid database URL ot the environment variable ``DATABASE_URL`` must be set to a
117
- valid database URL.
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
- When running this function for the first time, this should be set to True. When running this function again
120
- after the vehicles have been initialized, this should be set to False.
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
- :class:`eflips.model.Scenario` object, the environment variable `DATABASE_URL` must be set to a
123
- valid database URL.
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
- to True, the SoC at each stop is calculated and added to the "timeseries" column of the Event table. If this
126
- is set to False, the "timeseries" column of the Event table will be set to ``None``. Setting this to false
127
- may significantly speed up the simulation.
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
- time and power of the charging events in the database. The arrival and departure times and SoCs at these times are
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
- depot. The dictionary should have the following structure:
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() != vehicle_types_for_depot:
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 areas (except the area for the first standby process, which is already
663
- # really large), with a 2x margin
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 = sum(
666
- [
667
- area.capacity
668
- for area in depot.areas
669
- if area.vehicle_type_id == int(vehicle_type)
670
- and depot.default_plan.processes[0] not in area.processes
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
- the depot IDs, as strings.
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
- database.
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.values():
812
+ for depot_id, depot_evaluation in depot_evaluations.items():
789
813
  simulation_start_time = depot_evaluation.sim_start_datetime
790
814
 
791
- # Initialization of empty lists
815
+ # Depot-layer operations
792
816
 
793
817
  list_of_assigned_schedules = []
794
818
 
795
- # Read results from depot_evaluation categorized by vehicle
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
- # Generate process log for each
813
- list_of_finished_trips = current_vehicle.finished_trips
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
- for i in range(len(list_of_finished_trips)):
817
- if list_of_finished_trips[i].is_copy is False:
818
- current_trip = list_of_finished_trips[i]
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
- # Add all non-copy trips to the dictionary
821
- dict_of_events[current_trip.atd] = {
822
- "type": "Trip",
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
- # Match current trip to its serving vehicle
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
- waiting_area_id = (
903
- session.query(Area.id)
904
- .join(AssocAreaProcess, AssocAreaProcess.area_id == Area.id)
905
- .join(Process, Process.id == AssocAreaProcess.process_id)
906
- .filter(
907
- Process.dispatchable == False,
908
- # Must use "==" instead of "is". Or it would be recongnize as a python statement rather than a SQL statement
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
- # Make sure the vehicle is waiting at an area with enough capacity
884
+ # Python passes dictionaries by reference
920
885
 
921
- current_slot = slot_log[waiting_log_timekeys[idx - 1]]
886
+ _complete_standby_departure_events(dict_of_events, latest_time)
922
887
 
923
- start_time = end_time - waiting_info["waiting_time"]
888
+ _add_soc_to_events(dict_of_events, current_vehicle.battery_logs)
924
889
 
925
- warnings.warn(
926
- f"Vehicle {current_vehicle.ID} is waiting at {waiting_area_id} because area {expected_area} is full."
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
- dict_of_events[start_time] = {
930
- "type": "Standby",
931
- "end": end_time,
932
- "area": waiting_area_id,
933
- "slot": current_slot,
934
- "is_area_sink": False,
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
- # Create a list of battery log in order of time asc. Convenient for looking up corresponding soc
938
- battery_log_list = []
939
- for log in battery_log:
940
- battery_log_list.append((log.t, log.energy / log.energy_real))
941
-
942
- for start_time, process_log in current_vehicle.logger.loggedData[
943
- "dwd.active_processes_copy"
944
- ].items():
945
- if earliest_time < start_time < latest_time:
946
- if len(process_log) == 0:
947
- # A departure happens
948
- if last_standby_departure_start != 0:
949
- # Update the last standby-departure end time
950
- dict_of_events[last_standby_departure_start][
951
- "end"
952
- ] = start_time
953
- else:
954
- continue
955
-
956
- else:
957
- for process in process_log:
958
- match process.status:
959
- case ProcessStatus.COMPLETED | ProcessStatus.CANCELLED:
960
- assert (
961
- len(process.starts) == 1
962
- and len(process.ends) == 1
963
- ), (
964
- f"Current process {process.ID} is completed and should only contain one start and "
965
- f"one end time."
966
- )
967
- current_area = area_log[start_time]
968
- current_slot = slot_log[start_time]
969
-
970
- if current_area is None or current_slot is None:
971
- raise ValueError(
972
- f"For process {process.ID} Area and slot should not be None."
973
- )
974
-
975
- if process.dur > 0:
976
- # Valid duration
977
- dict_of_events[start_time] = {
978
- "type": type(process).__name__,
979
- "end": process.ends[0],
980
- "area": current_area.ID,
981
- "slot": current_slot,
982
- "id": process.ID,
983
- }
984
- else:
985
- # Duration is 0
986
- if current_area.issink is True:
987
- # Standby departure
988
- if start_time in dict_of_events:
989
- # Actual start time should be the end time of the other positive
990
- # duration process starting at the same time
991
- actual_start_time = dict_of_events[
992
- start_time
993
- ]["end"]
994
- else:
995
- for other_process in process_log:
996
- if (
997
- other_process.dur > 0
998
- and len(other_process.ends) != 0
999
- ):
1000
- actual_start_time = (
1001
- other_process.ends[0]
1002
- )
1003
- else:
1004
- # Invalid standby before a process in progress will be ignored
1005
- continue
1006
-
1007
- last_standby_departure_start = (
1008
- actual_start_time
1009
- )
1010
-
1011
- # If this standby event lasts actually 0 seconds, it is not a real event
1012
- if (
1013
- actual_start_time
1014
- in dict_of_events.keys()
1015
- and dict_of_events[actual_start_time][
1016
- "type"
1017
- ]
1018
- == "Trip"
1019
- ):
1020
- continue
1021
- dict_of_events[actual_start_time] = {
1022
- "type": type(process).__name__,
1023
- "area": current_area.ID,
1024
- "is_area_sink": current_area.issink,
1025
- "slot": current_slot,
1026
- "id": process.ID,
1027
- }
1028
-
1029
- else:
1030
- # Standby arrival
1031
- assert current_area.issink is False, (
1032
- f"A bus cannot go from Area {current_area.ID} to other areas. A Parking Area"
1033
- f" for standby arrival should be added."
1034
- )
1035
- dict_of_events[start_time] = {
1036
- "type": type(process).__name__,
1037
- "area": current_area.ID,
1038
- "is_area_sink": current_area.issink,
1039
- "slot": current_slot,
1040
- "id": process.ID,
1041
- }
1042
- case ProcessStatus.IN_PROGRESS:
1043
- assert (
1044
- len(process.starts) == 1
1045
- and len(process.ends) == 0
1046
- ), f"Current process {process.ID} is marked IN_PROGRESS, but has an end."
1047
- current_area = area_log[start_time]
1048
- current_slot = slot_log[start_time]
1049
-
1050
- if current_area is None or current_slot is None:
1051
- raise ValueError(
1052
- f"For process {process.ID} Area and slot should not be None."
1053
- )
1054
-
1055
- if process.dur > 0:
1056
- # Valid duration
1057
- dict_of_events[start_time] = {
1058
- "type": type(process).__name__,
1059
- "end": process.etc,
1060
- "area": current_area.ID,
1061
- "slot": current_slot,
1062
- "id": process.ID,
1063
- }
1064
- else:
1065
- raise NotImplementedError(
1066
- "We believe this should never happen. If it happens, handle it here."
1067
- )
1068
- case ProcessStatus.WAITING:
1069
- raise NotImplementedError(
1070
- f"Current process {process.ID} is waiting. Not implemented yet."
1071
- )
1072
-
1073
- case ProcessStatus.NOT_STARTED:
1074
- raise NotImplementedError(
1075
- f"Current process {process.ID} is not started. Not implemented yet."
1076
- )
1077
-
1078
- case _:
1079
- raise ValueError(
1080
- f"Invalid process status {process.status} for process {process.ID}."
1081
- )
1082
-
1083
- # Reverse the time keys to make generation of events before the trip easier
1084
- time_keys = sorted(dict_of_events.keys())
1085
- if len(time_keys) != 0:
1086
- # Generating valid event-list
1087
-
1088
- for start_time in time_keys:
1089
- process_dict = dict_of_events[start_time]
1090
-
1091
- # Generate EventType
1092
- match process_dict["type"]:
1093
- case "Serve":
1094
- event_type = EventType.SERVICE
1095
- case "Charge":
1096
- event_type = EventType.CHARGING_DEPOT
1097
- case "Standby":
1098
- if process_dict["is_area_sink"] is True:
1099
- event_type = EventType.STANDBY_DEPARTURE
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
- event_type = EventType.STANDBY
1102
- case "Precondition":
1103
- event_type = EventType.PRECONDITIONING
1104
- case "Trip":
1105
- continue
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
- 'Invalid process type %s. Valid process types are "Serve", "Charge", '
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
- session.add(current_event)
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
- # For non-copy schedules with no predecessor events, adding a dummy standby-departure
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
- dict_of_events[time_keys[0]]["type"] == "Trip"
1164
- and dict_of_events[time_keys[0]]["is_copy"] is False
1249
+ "is_waiting" in process_dict.keys()
1250
+ and process_dict["is_waiting"] is True
1165
1251
  ):
1166
- standby_start = time_keys[0] - 1
1167
- standby_end = time_keys[0]
1168
- rotation_id = int(dict_of_events[time_keys[0]]["id"])
1169
- area = (
1170
- session.query(Area)
1171
- .filter(Area.vehicle_type_id == vehicle_type_id)
1172
- .first()
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
- first_trip = (
1176
- session.query(Trip)
1177
- .filter(Trip.rotation_id == rotation_id)
1178
- .order_by(Trip.departure_time)
1179
- .first()
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
- soc = (
1183
- session.query(Event.soc_end)
1184
- .filter(Event.scenario == scenario)
1185
- .filter(Event.trip_id == first_trip.id)
1186
- .first()[0]
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
- standby_event = Event(
1190
- scenario=scenario,
1191
- vehicle_type_id=vehicle_type_id,
1192
- vehicle=current_vehicle_db,
1193
- station_id=None,
1194
- area_id=area.id,
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
- session.add(standby_event)
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
- # New rotation assignment
1210
- for schedule_id, vehicle_id in list_of_assigned_schedules:
1211
- # Get corresponding old vehicle id
1212
- session.query(Rotation).filter(Rotation.id == schedule_id).update(
1213
- {"vehicle_id": vehicle_id}, synchronize_session="auto"
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()