eflips-depot 4.3.16__py3-none-any.whl → 4.3.18__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  """This package contains the private API for the depot-related functionality in eFLIPS."""
2
-
2
+ import itertools
3
+ import logging
3
4
  from datetime import timedelta
4
5
  from enum import Enum, auto
5
6
  from math import ceil
@@ -25,6 +26,9 @@ from eflips.model import (
25
26
  )
26
27
  from sqlalchemy.orm import Session
27
28
 
29
+ from eflips.depot import DepotEvaluation, LineArea, DirectArea
30
+ from eflips.depot.evaluation import to_prev_values
31
+
28
32
 
29
33
  def delete_depots(scenario: Scenario, session: Session) -> None:
30
34
  """This function deletes all depot-related data from the database for a given scenario.
@@ -87,6 +91,10 @@ def depot_to_template(depot: Depot) -> Dict[str, str | Dict[str, str | int]]:
87
91
  # Helper for adding processes to the template
88
92
  list_of_processes = []
89
93
 
94
+ # Load all areas, sorted by their type
95
+ area_type_order = [AreaType.LINE, AreaType.DIRECT_ONESIDE, AreaType.DIRECT_TWOSIDE]
96
+ sorted_areas = sorted(depot.areas, key=lambda x: area_type_order.index(x.area_type))
97
+
90
98
  # Get dictionary of each area
91
99
  for area in depot.areas:
92
100
  area_name = str(area.id)
@@ -236,7 +244,7 @@ def depot_to_template(depot: Depot) -> Dict[str, str | Dict[str, str | int]]:
236
244
  }
237
245
  if process_type(process) == ProcessType.CHARGING:
238
246
  template["groups"][group_name]["typename"] = "ParkingAreaGroup"
239
- template["groups"][group_name]["parking_strategy_name"] = "SMART2"
247
+ template["groups"][group_name]["parking_strategy_name"] = "FIRST"
240
248
 
241
249
  # Fill in locations of the plan
242
250
  template["plans"]["default"]["locations"].append(group_name)
@@ -579,3 +587,358 @@ def _generate_all_direct_depot(
579
587
  safety_margin=0.2,
580
588
  shunting_duration=shunting_duration,
581
589
  )
590
+
591
+
592
+ def generate_line_depot_layout(
593
+ CLEAN_DURATION: int,
594
+ charging_power: float,
595
+ station: Station,
596
+ scenario: Scenario,
597
+ session: sqlalchemy.orm.session.Session,
598
+ direct_counts: Dict[VehicleType, int],
599
+ line_counts: Dict[VehicleType, int],
600
+ line_length: int,
601
+ vehicle_type_rotation_dict: Dict[VehicleType, List[Rotation]],
602
+ shunting_duration: timedelta = timedelta(minutes=5),
603
+ ) -> None:
604
+ """
605
+ Generate a depot layout with line areas and direct areas.
606
+
607
+ :param CLEAN_DURATION: The duration of the cleaning process in seconds.
608
+ :param charging_power: The charging power of the charging area in kW.
609
+ :param station: The stop where the depot is located.
610
+ :param scenario: The scenario for which the depot layout should be generated.
611
+ :param session: The SQLAlchemy session object.
612
+ :param direct_counts: A dictionary with vehicle types as keys and the number of vehicles in the direct areas as
613
+ values.
614
+ :param line_counts: A dictionary with vehicle types as keys and the number of vehicles in the line areas as values.
615
+ :param line_length: The length of the line areas.
616
+ :param vehicle_type_rotation_dict: A dictionary with vehicle types as keys and rotations as values.
617
+ :return: The number of cleaning areas and the number of shunting areas.
618
+ """
619
+ logger = logging.getLogger(__name__)
620
+ DEBUG_PLOT = False
621
+
622
+ # In order to figure out how many cleaning areas we need, we look at the number of vehicle simultaneously being
623
+ # cleaned. This is the number of vehicles simulatenously being within the "CLEAN_DURATION" after their arrival.
624
+
625
+ # We assemble a vector of all time in the simulation
626
+ logger.info("Calculating the number of cleaning areas needed")
627
+ all_rotations = list(itertools.chain(*vehicle_type_rotation_dict.values()))
628
+ start_time = min(
629
+ [rotation.trips[0].departure_time for rotation in all_rotations]
630
+ ).timestamp()
631
+ end_time = max(
632
+ [rotation.trips[-1].arrival_time for rotation in all_rotations]
633
+ ).timestamp()
634
+ timestamps_to_sample = np.arange(start_time, end_time, 60)
635
+ clean_occupancy = np.zeros_like(timestamps_to_sample)
636
+
637
+ # Then fir each arrival, we add 1 to the CLEAN_DURATION after the arrival
638
+ for rotation in all_rotations:
639
+ rotation_end = rotation.trips[-1].arrival_time.timestamp()
640
+ clean_occupancy += np.interp(
641
+ timestamps_to_sample,
642
+ [rotation_end, rotation_end + CLEAN_DURATION],
643
+ [1, 1],
644
+ left=0,
645
+ right=0,
646
+ )
647
+
648
+ if DEBUG_PLOT:
649
+ from matplotlib import pyplot as plt
650
+
651
+ plt.figure()
652
+ plt.plot(timestamps_to_sample, clean_occupancy)
653
+ plt.show()
654
+
655
+ vehicles_arriving_in_window = int(max(clean_occupancy))
656
+ logger.info(
657
+ f"Number of vehicles arriving in a {CLEAN_DURATION/60:.1f} minute window: {vehicles_arriving_in_window:.0f}"
658
+ )
659
+
660
+ # Take a fifth of the vehicles arriving in the window as the number of cleaning areas needed
661
+ clean_areas_needed = ceil(vehicles_arriving_in_window / 2)
662
+ logger.info(f"Number of cleaning areas created: {clean_areas_needed}")
663
+ del all_rotations, clean_occupancy, timestamps_to_sample, start_time, end_time
664
+
665
+ # Create the depot
666
+ # `vehicles_arriving_in_window`+1 will be the size of our shunting areas
667
+ # `clean_areas_needed` will be the size of our cleaning areas
668
+ # We will create line and direct areas for each vehicle type
669
+ # - THe line areas will be of length `line_length` and count `line_counts[vehicle_type]`
670
+ # - The direct areas will be of length 1 and count `direct_counts[vehicle_type]`
671
+ # - The charging power for the line areas will be `charging_power_direct` and for the direct areas `charging_power`
672
+ # unless `charging_power_direct` is not set, in which case `charging_power` will be used.
673
+
674
+ # Create the depot
675
+ depot = Depot(
676
+ scenario=scenario,
677
+ name=f"Depot at {station.name}",
678
+ name_short=station.name_short,
679
+ station_id=station.id,
680
+ )
681
+ session.add(depot)
682
+
683
+ shunting_1 = Process(
684
+ name="Shunting 1 (Arrival -> Cleaning)",
685
+ scenario=scenario,
686
+ dispatchable=False,
687
+ duration=shunting_duration,
688
+ )
689
+
690
+ session.add(shunting_1)
691
+ clean = Process(
692
+ name="Arrival Cleaning",
693
+ scenario=scenario,
694
+ dispatchable=False,
695
+ duration=timedelta(seconds=CLEAN_DURATION),
696
+ )
697
+ session.add(clean)
698
+
699
+ shunting_2 = Process(
700
+ name="Shunting 2 (Cleaning -> Charging)",
701
+ scenario=scenario,
702
+ dispatchable=False,
703
+ duration=shunting_duration,
704
+ )
705
+ session.add(shunting_2)
706
+
707
+ charging = Process(
708
+ name="Charging",
709
+ scenario=scenario,
710
+ dispatchable=True,
711
+ electric_power=charging_power,
712
+ )
713
+
714
+ standby_departure = Process(
715
+ name="Standby Pre-departure",
716
+ scenario=scenario,
717
+ dispatchable=True,
718
+ )
719
+ session.add(standby_departure)
720
+
721
+ # Create shared waiting area
722
+ # This will be the "virtual" area where vehicles wait for a spot in the depot
723
+ waiting_area = Area(
724
+ scenario=scenario,
725
+ name=f"Waiting Area for every type of vehicle",
726
+ depot=depot,
727
+ area_type=AreaType.DIRECT_ONESIDE,
728
+ capacity=100,
729
+ )
730
+ session.add(waiting_area)
731
+
732
+ # Create a shared shunting area (large enough to fit all rotations)
733
+ shunting_area_1 = Area(
734
+ scenario=scenario,
735
+ name=f"Shunting Area 1 (Arrival -> Cleaning)",
736
+ depot=depot,
737
+ area_type=AreaType.DIRECT_ONESIDE,
738
+ capacity=sum(
739
+ [len(rotations) for rotations in vehicle_type_rotation_dict.values()]
740
+ ), # TODO
741
+ )
742
+ session.add(shunting_area_1)
743
+ shunting_area_1.processes.append(shunting_1)
744
+
745
+ # Create a shared cleaning area
746
+ cleaning_area = Area(
747
+ scenario=scenario,
748
+ name=f"Cleaning Area",
749
+ depot=depot,
750
+ area_type=AreaType.DIRECT_ONESIDE,
751
+ capacity=clean_areas_needed,
752
+ )
753
+ session.add(cleaning_area)
754
+ cleaning_area.processes.append(clean)
755
+
756
+ # Create a shared shunting area
757
+ shunting_area_2 = Area(
758
+ scenario=scenario,
759
+ name=f"Shunting Area 2 (Cleaning -> Charging)",
760
+ depot=depot,
761
+ area_type=AreaType.DIRECT_ONESIDE,
762
+ capacity=clean_areas_needed,
763
+ )
764
+ session.add(shunting_area_2)
765
+ shunting_area_2.processes.append(shunting_2)
766
+
767
+ # Create the line areas for each vehicle type
768
+ for vehicle_type, count in line_counts.items():
769
+ for i in range(count):
770
+ line_area = Area(
771
+ scenario=scenario,
772
+ name=f"Line Area for {vehicle_type.name} #{i+1:02d}",
773
+ depot=depot,
774
+ area_type=AreaType.LINE,
775
+ capacity=line_length,
776
+ vehicle_type=vehicle_type,
777
+ )
778
+ session.add(line_area)
779
+ line_area.processes.append(charging)
780
+ line_area.processes.append(standby_departure)
781
+
782
+ # Create the direct areas for each vehicle type
783
+ for vehicle_type, count in direct_counts.items():
784
+ if count > 0:
785
+ direct_area = Area(
786
+ scenario=scenario,
787
+ name=f"Direct Area for {vehicle_type.name}",
788
+ depot=depot,
789
+ area_type=AreaType.DIRECT_ONESIDE,
790
+ capacity=count,
791
+ vehicle_type=vehicle_type,
792
+ )
793
+ session.add(direct_area)
794
+ direct_area.processes.append(charging)
795
+ direct_area.processes.append(standby_departure)
796
+
797
+ # Create the plan
798
+ # Create plan
799
+ plan = Plan(scenario=scenario, name=f"Default Plan")
800
+ session.add(plan)
801
+
802
+ depot.default_plan = plan
803
+
804
+ # Create the assocs in order to put the areas in the plan
805
+ assocs = [
806
+ AssocPlanProcess(scenario=scenario, process=shunting_1, plan=plan, ordinal=0),
807
+ AssocPlanProcess(scenario=scenario, process=clean, plan=plan, ordinal=1),
808
+ AssocPlanProcess(scenario=scenario, process=shunting_2, plan=plan, ordinal=2),
809
+ AssocPlanProcess(scenario=scenario, process=charging, plan=plan, ordinal=3),
810
+ AssocPlanProcess(
811
+ scenario=scenario, process=standby_departure, plan=plan, ordinal=4
812
+ ),
813
+ ]
814
+ session.add_all(assocs)
815
+
816
+
817
+ def real_peak_area_utilization(ev: DepotEvaluation) -> Dict[str, Dict[AreaType, int]]:
818
+ """
819
+ Calculate the real peak vehicle count for a depot evaluation by vehicle type and area type.
820
+
821
+ For the line areas, the maximum number of lines in use at the same time is calculated.
822
+
823
+ :param ev: A DepotEvaluation object.
824
+ :return: The real peak vehicle count by vehicle type and area type.
825
+ """
826
+ area_types_by_id: Dict[int, AreaType] = dict()
827
+ total_counts_by_area: Dict[str, Dict[str, np.ndarray]] = dict()
828
+
829
+ # We are assuming that the smulation runs for at least four days
830
+ SECONDS_IN_A_DAY = 24 * 60 * 60
831
+ assert ev.SIM_TIME >= 4 * SECONDS_IN_A_DAY
832
+
833
+ for area in ev.depot.list_areas:
834
+ # We need to figure out which kind of area this is
835
+ # We do this by looking at the vehicle type of the area
836
+ if len(area.entry_filter.filters) > 0:
837
+ if isinstance(area, LineArea):
838
+ area_types_by_id[area.ID] = AreaType.LINE
839
+ elif isinstance(area, DirectArea):
840
+ area_types_by_id[area.ID] = AreaType.DIRECT_ONESIDE
841
+ else:
842
+ raise ValueError("Unknown area type")
843
+
844
+ assert len(area.entry_filter.vehicle_types_str) == 1
845
+ vehicle_type_name = area.entry_filter.vehicle_types_str[0]
846
+
847
+ nv = area.logger.get_valList("count", SIM_TIME=ev.SIM_TIME)
848
+ nv = to_prev_values(nv)
849
+ nv = np.array(nv)
850
+
851
+ # If the area is empty, we don't care about it
852
+ if np.all(nv == 0):
853
+ continue
854
+
855
+ if vehicle_type_name not in total_counts_by_area:
856
+ total_counts_by_area[vehicle_type_name] = dict()
857
+ # We don't want the last day, as all vehicles will re-enter the depot
858
+ total_counts_by_area[vehicle_type_name][area.ID] = nv[:-SECONDS_IN_A_DAY]
859
+ else:
860
+ # This is an area for all vehicle types
861
+ # We don't care about this
862
+ continue
863
+
864
+ if False:
865
+ from matplotlib import pyplot as plt
866
+
867
+ for vehicle_type_name, counts in total_counts_by_area.items():
868
+ plt.figure()
869
+ for area_id, proper_counts in counts.items():
870
+ # dashed if direct, solid if line
871
+ if area_types_by_id[area_id] == AreaType.DIRECT_ONESIDE:
872
+ plt.plot(proper_counts, "--", label=area_id)
873
+ else:
874
+ plt.plot(proper_counts, label=area_id)
875
+ plt.legend()
876
+ plt.show()
877
+
878
+ # Calculate the maximum utilization of the direct areas and the maximum number of lines in use at the same time
879
+ # Per vehicle type
880
+ ret_val: Dict[str, Dict[AreaType, int]] = dict()
881
+ for vehicle_type_name, count_dicts in total_counts_by_area.items():
882
+ peak_direct_area_usage = 0
883
+ number_of_lines_in_use = 0
884
+ for area_id, counts in count_dicts.items():
885
+ if area_types_by_id[area_id] == AreaType.DIRECT_ONESIDE:
886
+ peak_direct_area_usage += max(peak_direct_area_usage, np.max(counts))
887
+ else:
888
+ number_of_lines_in_use += 1
889
+
890
+ ret_val[vehicle_type_name] = {
891
+ AreaType.DIRECT_ONESIDE: int(peak_direct_area_usage),
892
+ AreaType.LINE: int(number_of_lines_in_use),
893
+ }
894
+
895
+ return ret_val
896
+
897
+
898
+ def real_peak_vehicle_count(ev: DepotEvaluation) -> Dict[str, int]:
899
+ """
900
+ Calculate the real peak vehicle count for a depot evaluation.
901
+
902
+ This is different from the amount of vehicles used
903
+ in the calculation, as towards the end of the simulation all vehicles will re-enter-the depot, which leads to
904
+ a lower actual peak vehicle count than what `nvehicles_used_calculation` returns.
905
+ :param ev: A DepotEvaluation object.
906
+ :return: The real peak vehicle count. This is what the depot layout should be designed for.
907
+ """
908
+
909
+ total_counts_by_vehicle_type: Dict[str, np.ndarray] = dict()
910
+
911
+ for area in ev.depot.list_areas:
912
+ # We need to figure out which kind of area this is
913
+ # We do this by looking at the vehicle type of the area
914
+ if len(area.entry_filter.filters) > 0:
915
+ assert len(area.entry_filter.vehicle_types_str) == 1
916
+ vehicle_type_name = area.entry_filter.vehicle_types_str[0]
917
+
918
+ nv = area.logger.get_valList("count", SIM_TIME=ev.SIM_TIME)
919
+ nv = to_prev_values(nv)
920
+ nv = np.array(nv)
921
+
922
+ if vehicle_type_name not in total_counts_by_vehicle_type:
923
+ total_counts_by_vehicle_type[vehicle_type_name] = np.zeros(
924
+ ev.SIM_TIME, dtype=np.int32
925
+ )
926
+ total_counts_by_vehicle_type[vehicle_type_name] += nv
927
+ else:
928
+ # This is an area for all vehicle types
929
+ # We don't care about this
930
+ continue
931
+
932
+ # We are assuming that the smulation runs for at least four days
933
+ SECONDS_IN_A_DAY = 24 * 60 * 60
934
+ assert ev.SIM_TIME >= 4 * SECONDS_IN_A_DAY
935
+
936
+ # Towards the end, all the vehicles will re-enter the depot
937
+ # So our practital peak vehicle count is the maximum excluding the last day
938
+ for vehicle_type_name, counts in total_counts_by_vehicle_type.items():
939
+ total_counts_by_vehicle_type[vehicle_type_name] = counts[:-SECONDS_IN_A_DAY]
940
+
941
+ return {
942
+ vehicle_type_name: int(np.max(counts))
943
+ for vehicle_type_name, counts in total_counts_by_vehicle_type.items()
944
+ }