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.
- eflips/depot/api/__init__.py +19 -979
- eflips/depot/api/private/depot.py +365 -2
- eflips/depot/api/private/results_to_database.py +586 -0
- {eflips_depot-4.3.16.dist-info → eflips_depot-4.3.18.dist-info}/METADATA +2 -1
- {eflips_depot-4.3.16.dist-info → eflips_depot-4.3.18.dist-info}/RECORD +7 -6
- {eflips_depot-4.3.16.dist-info → eflips_depot-4.3.18.dist-info}/WHEEL +1 -1
- {eflips_depot-4.3.16.dist-info → eflips_depot-4.3.18.dist-info}/LICENSE.md +0 -0
|
@@ -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"] = "
|
|
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
|
+
}
|