eflips-depot 4.13.0__tar.gz → 4.13.2__tar.gz

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.
Files changed (42) hide show
  1. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/PKG-INFO +1 -1
  2. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/__init__.py +43 -3
  3. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/api/__init__.py +176 -85
  4. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/api/private/depot.py +14 -3
  5. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/api/private/results_to_database.py +54 -34
  6. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/pyproject.toml +1 -1
  7. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/LICENSE.md +0 -0
  8. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/README.md +0 -0
  9. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/api/defaults/default_settings.json +0 -0
  10. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/api/private/__init__.py +0 -0
  11. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/api/private/consumption.py +0 -0
  12. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/api/private/util.py +0 -0
  13. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/configuration.py +0 -0
  14. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/depot.py +0 -0
  15. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/evaluation.py +0 -0
  16. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/filters.py +0 -0
  17. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/input_epex_power_price.py +0 -0
  18. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/layout_opt/__init__.py +0 -0
  19. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/layout_opt/doc/__init__.py +0 -0
  20. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/layout_opt/doc/direct_details.pdf +0 -0
  21. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/layout_opt/evaluation.py +0 -0
  22. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/layout_opt/opt_tools/__init__.py +0 -0
  23. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/layout_opt/opt_tools/crossover.py +0 -0
  24. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/layout_opt/opt_tools/fitness_c_urfd.py +0 -0
  25. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/layout_opt/opt_tools/fitness_util.py +0 -0
  26. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/layout_opt/opt_tools/init.py +0 -0
  27. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/layout_opt/opt_tools/mutation.py +0 -0
  28. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/layout_opt/optimize_c_urfd.py +0 -0
  29. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/layout_opt/packing.py +0 -0
  30. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/layout_opt/settings.py +0 -0
  31. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/layout_opt/template_creation.py +0 -0
  32. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/layout_opt/util.py +0 -0
  33. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/plots.py +0 -0
  34. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/processes.py +0 -0
  35. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/rating.py +0 -0
  36. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/resources.py +0 -0
  37. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/settings_config.py +0 -0
  38. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/simple_vehicle.py +0 -0
  39. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/simulation.py +0 -0
  40. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/smart_charging.py +0 -0
  41. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/standalone.py +0 -0
  42. {eflips_depot-4.13.0 → eflips_depot-4.13.2}/eflips/depot/validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: eflips-depot
3
- Version: 4.13.0
3
+ Version: 4.13.2
4
4
  Summary: Depot Simulation for eFLIPS
5
5
  License: AGPL-3.0-or-later
6
6
  Author: Enrico Lauth
@@ -54,9 +54,49 @@ from eflips.depot.standalone import VehicleGenerator, SimpleTrip, Timetable
54
54
  from eflips.depot.validation import Validator
55
55
 
56
56
 
57
+ class DelayedTripException(Exception):
58
+ def __init__(self):
59
+ self._delayed_trips = []
60
+
61
+ def raise_later(self, simple_trip):
62
+ self._delayed_trips.append(simple_trip)
63
+
64
+ @property
65
+ def has_errors(self):
66
+ return len(self._delayed_trips) > 0
67
+
68
+ def __str__(self):
69
+ trip_names = ", ".join(
70
+ f"{trip.ID} originally departure at {trip.std}"
71
+ for trip in self._delayed_trips
72
+ )
73
+
74
+ return (
75
+ f"The following blocks/rotations are delayed. "
76
+ f"Ignoring this error will write related depot events into database. However, this may lead to errors due "
77
+ f"to conflicts with driving events: {trip_names}"
78
+ )
79
+
80
+
57
81
  class UnstableSimulationException(Exception):
58
- pass
82
+ def __init__(self):
83
+ self._unstable_trips = []
59
84
 
85
+ def raise_later(self, simple_trip):
86
+ self._unstable_trips.append(simple_trip)
60
87
 
61
- class DelayedTripException(Exception):
62
- pass
88
+ @property
89
+ def has_errors(self):
90
+ return len(self._unstable_trips) > 0
91
+
92
+ def __str__(self):
93
+ trip_names = ", ".join(trip.ID for trip in self._unstable_trips)
94
+ return (
95
+ f"The following blocks/rotations require a new vehicle. This suggests an unstable "
96
+ f" simulation result, where a repeated schedule might require more vehicles: {trip_names}"
97
+ )
98
+
99
+
100
+ class MultipleErrors(Exception):
101
+ def __init__(self, errors):
102
+ self.errors = errors
@@ -29,11 +29,10 @@ import logging
29
29
  import os
30
30
  import warnings
31
31
  from collections import OrderedDict
32
- from dataclasses import dataclass
33
32
  from datetime import timedelta, datetime
34
33
  from enum import Enum
35
34
  from math import ceil
36
- from typing import Any, Dict, Optional, Union, List
35
+ from typing import Any, Dict, Optional, Union
37
36
 
38
37
  import sqlalchemy.orm
39
38
  from eflips.model import (
@@ -51,14 +50,17 @@ from eflips.model import (
51
50
  Route,
52
51
  ConsistencyWarning,
53
52
  Station,
54
- ConsumptionLut,
55
53
  )
54
+ from sqlalchemy import func
56
55
  from sqlalchemy.orm import Session
57
56
 
58
57
  import eflips.depot
59
58
  from eflips.depot import (
60
59
  DepotEvaluation,
61
60
  SimulationHost,
61
+ UnstableSimulationException,
62
+ DelayedTripException,
63
+ MultipleErrors,
62
64
  )
63
65
  from eflips.depot.api.private.consumption import ConsumptionResult
64
66
  from eflips.depot.api.private.consumption import (
@@ -637,16 +639,30 @@ def simulate_scenario(
637
639
  ev = run_simulation(simulation_host)
638
640
  try:
639
641
  add_evaluation_to_database(scenario, ev, session)
640
- except eflips.depot.UnstableSimulationException as e:
641
- if ignore_unstable_simulation:
642
- logger.warning("Simulation is unstable. Continuing.")
643
- else:
644
- raise e
645
- except eflips.depot.DelayedTripException as e:
646
- if ignore_delayed_trips:
647
- logger.warning("Simulation has delayed trips. Continuing.")
648
- else:
649
- raise e
642
+
643
+ except MultipleErrors as e:
644
+ if e.errors:
645
+ for error in e.errors:
646
+ if (
647
+ isinstance(error, DelayedTripException)
648
+ and not ignore_delayed_trips
649
+ ):
650
+ logger.error(
651
+ "There are delayed trips in the simulation. "
652
+ "Please check the input data and try again."
653
+ )
654
+ raise error
655
+ if (
656
+ isinstance(error, UnstableSimulationException)
657
+ and not ignore_unstable_simulation
658
+ ):
659
+ logger.error(
660
+ "Simulation became unstable. "
661
+ "Please check the input data and try again."
662
+ )
663
+ raise error
664
+
665
+ logger.warning("An error occurred during the simulation: %s", error)
650
666
 
651
667
  match smart_charging_strategy:
652
668
  case SmartChargingStrategy.NONE:
@@ -746,28 +762,15 @@ def init_simulation(
746
762
  .all()
747
763
  ]
748
764
 
749
- first_departure_time = min(
750
- [vehicle_schedule.departure for vehicle_schedule in vehicle_schedules]
751
- )
752
- last_arrival_time = max(
753
- [vehicle_schedule.arrival for vehicle_schedule in vehicle_schedules]
754
- )
755
-
756
- # We take first arrival time as simulation start
757
- total_duration = (last_arrival_time - first_departure_time).total_seconds()
758
- schedule_duration_days = ceil(total_duration / (24 * 60 * 60))
759
-
760
- if repetition_period is None and schedule_duration_days in [1, 2]:
761
- repetition_period = timedelta(days=1)
762
- elif repetition_period is None and schedule_duration_days in [7, 8]:
763
- repetition_period = timedelta(weeks=1)
764
- elif repetition_period is None:
765
- raise ValueError(
766
- "Could not automatically detect repetition period. Please specify manually."
767
- )
765
+ if repetition_period is None:
766
+ repetition_period = schedule_duration_days(scenario)
767
+ if repetition_period not in (timedelta(days=1), timedelta(days=7)):
768
+ warnings.warn(
769
+ f"Non-standard schedule duration of {repetition_period}. Please make sure this is intended.",
770
+ UserWarning,
771
+ )
768
772
 
769
773
  # Now, we need to repeat the vehicle schedules
770
-
771
774
  vehicle_schedules = repeat_vehicle_schedules(vehicle_schedules, repetition_period)
772
775
 
773
776
  sim_start_stime, total_duration_seconds = start_and_end_times(vehicle_schedules)
@@ -784,6 +787,8 @@ def init_simulation(
784
787
  # Clear old vehicle counts, if they exist
785
788
  eflips.globalConstants["depot"]["vehicle_count"] = {}
786
789
 
790
+ grouped_rotations = group_rotations_by_start_end_stop(scenario.id, session)
791
+
787
792
  # We need to calculate roughly how many vehicles we need for each depot
788
793
  for depot in session.query(Depot).filter(Depot.scenario_id == scenario.id).all():
789
794
  depot_id = str(depot.id)
@@ -806,31 +811,32 @@ def init_simulation(
806
811
  depot_id
807
812
  ] = vehicle_count_dict[depot_id]
808
813
  else:
809
- # Calculate it from the size of the charging area with a 2x margin
814
+ # Calculate it from the amount of rotations with a 4x margin because 4 times of repetition
815
+ # in repeat_vehicle_schedules()
816
+ rotations = grouped_rotations[(depot.station, depot.station)]
810
817
 
811
818
  for vehicle_type in vehicle_types_for_depot:
812
- vehicle_count = 0
813
- for area in depot.areas:
814
- if (
815
- area.vehicle_type_id == int(vehicle_type)
816
- or area.vehicle_type_id is None
817
- ):
818
- # The areas allow either one type, or all vehicle types
819
- for p in area.processes:
820
- if p.electric_power is not None and p.duration is None:
821
- row_count = (
822
- area.row_count if area.row_count is not None else 1
823
- )
824
-
825
- vehicle_count += area.capacity * row_count
826
-
827
- assert (
828
- vehicle_count > 0
829
- ), f"The charging area capacity for vehicle type {vehicle_type} should not be 0."
830
-
831
- eflips.globalConstants["depot"]["vehicle_count"][depot_id][
832
- vehicle_type
833
- ] = (vehicle_count * 2)
819
+ vehicle_type_object = (
820
+ session.query(VehicleType)
821
+ .filter(
822
+ VehicleType.id == vehicle_type,
823
+ VehicleType.scenario_id == scenario.id,
824
+ )
825
+ .one()
826
+ )
827
+ vehicle_count = len(rotations.get(vehicle_type_object, []))
828
+
829
+ if vehicle_count > 0:
830
+ eflips.globalConstants["depot"]["vehicle_count"][depot_id][
831
+ vehicle_type
832
+ ] = (
833
+ vehicle_count
834
+ * 4 # We multiply by 4 because we repeat the vehicle schedules 4 times
835
+ )
836
+ else:
837
+ warnings.warn(
838
+ f"There are no rotations assigned to type {vehicle_type_object} in depot {depot_id}"
839
+ )
834
840
 
835
841
  # We need to put the vehicle type objects into the GlobalConstants
836
842
  for vehicle_type in (
@@ -1023,6 +1029,9 @@ def add_evaluation_to_database(
1023
1029
  f"one waiting area."
1024
1030
  )
1025
1031
 
1032
+ unstable_exp = UnstableSimulationException()
1033
+ delay_exp = DelayedTripException()
1034
+
1026
1035
  for current_vehicle in depot_evaluation.vehicle_generator.items:
1027
1036
  # Vehicle-layer operations
1028
1037
 
@@ -1049,23 +1058,16 @@ def add_evaluation_to_database(
1049
1058
  # handled. It is usually the departure time of the last copy trip in the "early-shifted" copy
1050
1059
  # schedules and the departure time of the first copy trip in the "late-shifted" copy schedules.
1051
1060
  ) = get_finished_schedules_per_vehicle(
1052
- dict_of_events, current_vehicle.finished_trips, current_vehicle_db.id
1061
+ dict_of_events,
1062
+ current_vehicle.finished_trips,
1063
+ current_vehicle_db.id,
1064
+ unstable_exp,
1065
+ delay_exp,
1053
1066
  )
1054
1067
 
1055
- try:
1056
- assert earliest_time is not None and latest_time is not None
1057
-
1058
- except AssertionError as e:
1059
- warnings.warn(
1060
- f"Vehicle {current_vehicle_db.id} has only copied trips. The profiles of this vehicle "
1061
- f"will not be written into database."
1062
- )
1068
+ if schedule_current_vehicle is None:
1063
1069
  continue
1064
1070
 
1065
- assert (
1066
- earliest_time < latest_time
1067
- ), f"Earliest time {earliest_time} is not less than latest time {latest_time}."
1068
-
1069
1071
  list_of_assigned_schedules.extend(schedule_current_vehicle)
1070
1072
 
1071
1073
  generate_vehicle_events(
@@ -1082,16 +1084,6 @@ def add_evaluation_to_database(
1082
1084
 
1083
1085
  add_soc_to_events(dict_of_events, current_vehicle.battery_logs)
1084
1086
 
1085
- try:
1086
- assert (not dict_of_events) is False
1087
- except AssertionError as e:
1088
- warnings.warn(
1089
- f"Vehicle {current_vehicle_db.id} has no valid events. The vehicle will not be written "
1090
- f"into database."
1091
- )
1092
-
1093
- continue
1094
-
1095
1087
  add_events_into_database(
1096
1088
  current_vehicle_db,
1097
1089
  dict_of_events,
@@ -1104,6 +1096,16 @@ def add_evaluation_to_database(
1104
1096
  update_vehicle_in_rotation(session, scenario, list_of_assigned_schedules)
1105
1097
  update_waiting_events(session, scenario, waiting_area_id)
1106
1098
 
1099
+ errors = []
1100
+
1101
+ if delay_exp.has_errors:
1102
+ errors.append(delay_exp)
1103
+ if unstable_exp.has_errors:
1104
+ errors.append(unstable_exp)
1105
+
1106
+ if len(errors) > 0:
1107
+ raise MultipleErrors(errors)
1108
+
1107
1109
 
1108
1110
  def generate_depot_optimal_size(
1109
1111
  scenario: Union[Scenario, int, Any],
@@ -1112,9 +1114,12 @@ def generate_depot_optimal_size(
1112
1114
  database_url: Optional[str] = None,
1113
1115
  delete_existing_depot: bool = False,
1114
1116
  use_consumption_lut: bool = False,
1117
+ repetition_period: Optional[timedelta] = None,
1115
1118
  ) -> None:
1116
1119
  """
1117
- Generates an optimal depot layout with the smallest possible size for each depot in the scenario. Line charging areas
1120
+ Generates an optimal depot layout with the smallest possible size for each depot in the scenario.
1121
+
1122
+ Line charging areas
1118
1123
  with given block length area preferred. The existing depot will be deleted if `delete_existing_depot` is set to True.
1119
1124
 
1120
1125
  :param scenario: Either a :class:`eflips.model.Scenario` object containing the input data for the simulation. Or
@@ -1125,11 +1130,14 @@ def generate_depot_optimal_size(
1125
1130
  :param database_url: An optional database URL. Used if no database url is given by the environment variable.
1126
1131
  :param delete_existing_depot: If there is already a depot existing in this scenario, set True to delete this
1127
1132
  existing depot. Set to False and a ValueError will be raised if there is a depot in this scenario.
1128
- :param using_consumption_lut: If True, the depot layout will be generated based on the consumption lookup table.
1133
+ :param use_consumption_lut: If True, the depot layout will be generated based on the consumption lookup table.
1129
1134
  If False, constant consumption stored in VehicleType table will be used.
1135
+ :param repetition_period: An optional timedelta object specifying the period of the vehicle schedules. If not
1136
+ specified, a default repetition period will be generated in simulate_scenario(). If the depot layout generated
1137
+ in this function will be used for further simulations, make sure that the repetition period is set to the same
1138
+ value as in the simulation.
1130
1139
 
1131
1140
  :return: None. The depot layout will be added to the database.
1132
-
1133
1141
  """
1134
1142
 
1135
1143
  logger = logging.getLogger(__name__)
@@ -1183,10 +1191,12 @@ def generate_depot_optimal_size(
1183
1191
 
1184
1192
  num_rotations_for_scenario: Dict[Station, int] = {}
1185
1193
 
1194
+ grouped_rotations = group_rotations_by_start_end_stop(scenario.id, session)
1195
+
1186
1196
  for (
1187
1197
  first_last_stop_tup,
1188
1198
  vehicle_type_dict,
1189
- ) in group_rotations_by_start_end_stop(scenario.id, session).items():
1199
+ ) in grouped_rotations.items():
1190
1200
  first_stop, last_stop = first_last_stop_tup
1191
1201
  if first_stop != last_stop:
1192
1202
  raise ValueError("First and last stop of a rotation are not the same.")
@@ -1230,6 +1240,7 @@ def generate_depot_optimal_size(
1230
1240
  session,
1231
1241
  standard_block_length,
1232
1242
  charging_power,
1243
+ repetition_period,
1233
1244
  )
1234
1245
 
1235
1246
  depot_capacities_for_scenario[station] = vt_capacities_for_station
@@ -1239,8 +1250,44 @@ def generate_depot_optimal_size(
1239
1250
 
1240
1251
  outer_savepoint.rollback()
1241
1252
 
1253
+ # Estimation of the number of shunting and cleaning slots
1254
+
1242
1255
  # Create depot using the calculated capacities
1243
1256
  for depot_station, capacities in depot_capacities_for_scenario.items():
1257
+ vehicle_type_rot_dict = grouped_rotations[depot_station, depot_station]
1258
+
1259
+ all_rotations_this_depot = []
1260
+
1261
+ for vehicle_type, rotations in vehicle_type_rot_dict.items():
1262
+ all_rotations_this_depot.extend(rotations)
1263
+
1264
+ # sort the rotations by their start time
1265
+ all_rotations_this_depot.sort(key=lambda r: r.trips[0].departure_time)
1266
+
1267
+ start_time = all_rotations_this_depot[0].trips[0].departure_time
1268
+ end_time = all_rotations_this_depot[-1].trips[-1].arrival_time
1269
+
1270
+ elapsed_time = (end_time - start_time).total_seconds()
1271
+ # make them into a numpy with 30 min resolution
1272
+ import numpy as np
1273
+
1274
+ TIME_RESOLUTION = 30 * 60 # 30 minutes in seconds
1275
+
1276
+ time_range = np.zeros(int(elapsed_time / TIME_RESOLUTION) + 1)
1277
+ # calculate the number of rotations per time slot
1278
+ for rot in all_rotations_this_depot:
1279
+ start_time_index = int(
1280
+ (rot.trips[0].departure_time - start_time).total_seconds()
1281
+ // TIME_RESOLUTION
1282
+ )
1283
+ end_time_index = int(
1284
+ (rot.trips[-1].arrival_time - start_time).total_seconds()
1285
+ // TIME_RESOLUTION
1286
+ )
1287
+ # interpolate the start and end time to the time range
1288
+
1289
+ time_range[start_time_index : end_time_index + 1] += 1
1290
+
1244
1291
  generate_depot(
1245
1292
  capacities,
1246
1293
  depot_station,
@@ -1248,6 +1295,50 @@ def generate_depot_optimal_size(
1248
1295
  session,
1249
1296
  standard_block_length=standard_block_length,
1250
1297
  charging_power=charging_power,
1251
- num_shunting_slots=num_rotations_for_scenario[depot_station] // 10,
1252
- num_cleaning_slots=num_rotations_for_scenario[depot_station] // 10,
1298
+ num_shunting_slots=int(max(time_range)),
1299
+ num_cleaning_slots=int(max(time_range)),
1253
1300
  )
1301
+
1302
+
1303
+ def schedule_duration_days(
1304
+ scenario: Union[Scenario, int, Any], database_url: Optional[str] = None
1305
+ ) -> timedelta:
1306
+ """
1307
+ This method calculates the duration of a given scenario in days.
1308
+
1309
+ This is the duration
1310
+ between the first departure of the first day, and the last departure on the last day, rounded up to full days.
1311
+
1312
+ Most of the time, this is the "natural" repetition period of the scenario. We are simulating one full period, and
1313
+ this period – continuously repeated – is what happens in reality.
1314
+
1315
+ This method can be used to show the user what the detected repetition period is and to auto-set the repetition
1316
+ period if none is provided.
1317
+
1318
+ :param scenario: Either a :class:`eflips.model.Scenario` object containing the input data for the simulation. Or
1319
+ an integer specifying the ID of a scenario in the database. Or any other object that has an attribute "id"
1320
+ containing an integer pointing to a unique scenario id.
1321
+ :param database_url: An optional database URL. Used if no database url is given by the environment variable.
1322
+ :return: a timedelta object representing the duration in days.
1323
+ """
1324
+ with create_session(scenario, database_url) as (session, scenario):
1325
+ first_departure = (
1326
+ session.query(func.min(Trip.departure_time))
1327
+ .filter(Trip.scenario_id == scenario.id)
1328
+ .scalar()
1329
+ )
1330
+ if first_departure is None:
1331
+ raise ValueError("No trips found in the scenario.")
1332
+
1333
+ last_departure = (
1334
+ session.query(func.max(Trip.departure_time))
1335
+ .filter(Trip.scenario_id == scenario.id)
1336
+ .scalar()
1337
+ )
1338
+ if last_departure is None:
1339
+ raise ValueError("No trips found in the scenario.")
1340
+
1341
+ duration = last_departure - first_departure
1342
+ duration_days = ceil(duration.total_seconds() / (24 * 60 * 60))
1343
+
1344
+ return timedelta(days=duration_days)
@@ -3,7 +3,7 @@ import logging
3
3
  import math
4
4
  from datetime import timedelta
5
5
  from enum import Enum, auto
6
- from typing import Dict, List, Tuple
6
+ from typing import Dict, List, Tuple, Optional
7
7
 
8
8
  import numpy as np
9
9
  import sqlalchemy.orm
@@ -583,12 +583,17 @@ def generate_depot(
583
583
  session.add_all(assocs) # It's complete, so add all at once
584
584
 
585
585
  # Create shared waiting area
586
+ rotation_count = len(
587
+ session.query(Rotation).filter(Rotation.scenario_id == scenario.id).all()
588
+ )
589
+
586
590
  waiting_area = Area(
587
591
  scenario=scenario,
588
592
  name=f"Waiting Area for every type of vehicle",
589
593
  depot=depot,
590
594
  area_type=AreaType.DIRECT_ONESIDE,
591
- capacity=100,
595
+ capacity=rotation_count
596
+ * 4, # Initialize with 4 times of rotation count because all rotations are copied three times. Assuming each rotation needs a vehicle.
592
597
  )
593
598
  session.add(waiting_area)
594
599
 
@@ -849,6 +854,7 @@ def depot_smallest_possible_size(
849
854
  session: sqlalchemy.orm.session.Session,
850
855
  standard_block_length: int = 6,
851
856
  charging_power: float = 90,
857
+ repetition_period: Optional[timedelta] = None,
852
858
  ) -> Dict[VehicleType, Dict[AreaType, None | int]]:
853
859
  """
854
860
  Identifies the smallest (in terms of area footprint) depot that can still fit the required vehicles.
@@ -919,7 +925,12 @@ def depot_smallest_possible_size(
919
925
  from eflips.depot.api import SmartChargingStrategy
920
926
 
921
927
  # Simulate the depot
922
- simulate_scenario(scenario, smart_charging_strategy=SmartChargingStrategy.NONE)
928
+
929
+ simulate_scenario(
930
+ scenario,
931
+ smart_charging_strategy=SmartChargingStrategy.NONE,
932
+ repetition_period=repetition_period,
933
+ )
923
934
 
924
935
  # Find the peak usage of the depot
925
936
  peak_occupancies: Dict[VehicleType, Dict[AreaType, int]] = find_peak_usage(
@@ -1,6 +1,7 @@
1
1
  import datetime
2
2
  import itertools
3
3
  import logging
4
+ import warnings
4
5
  from datetime import timedelta
5
6
  from typing import List, Dict
6
7
 
@@ -13,7 +14,11 @@ from eflips.depot import UnstableSimulationException, DelayedTripException
13
14
 
14
15
 
15
16
  def get_finished_schedules_per_vehicle(
16
- dict_of_events, list_of_finished_trips: List, db_vehicle_id: int
17
+ dict_of_events,
18
+ list_of_finished_trips: List,
19
+ db_vehicle_id: int,
20
+ unstable_exp: UnstableSimulationException,
21
+ delay_exp: DelayedTripException,
17
22
  ):
18
23
  """
19
24
  This function completes the following tasks:
@@ -42,47 +47,62 @@ def get_finished_schedules_per_vehicle(
42
47
  :return: A tuple of three elements. The first element is a list of finished schedules of the vehicle. The second and
43
48
  third elements are the earliest and latest time of the vehicle's schedules.
44
49
  """
45
- finished_schedules = []
46
50
 
47
51
  list_of_finished_trips.sort(key=lambda x: x.atd)
48
- earliest_time = None
49
- latest_time = None
50
52
 
51
53
  for i in range(len(list_of_finished_trips)):
52
- try:
53
- assert list_of_finished_trips[i].atd == list_of_finished_trips[i].std
54
- except AssertionError:
55
- raise DelayedTripException(
56
- f"The trip {list_of_finished_trips[i].ID} is delayed. The simulation doesn't "
57
- "support delayed trips for now."
58
- )
54
+ if list_of_finished_trips[i].atd != list_of_finished_trips[i].std:
55
+ delay_exp.raise_later(list_of_finished_trips[i])
59
56
 
60
- if list_of_finished_trips[i].is_copy is False:
61
- current_trip = list_of_finished_trips[i]
57
+ non_copy_trips = [trip for trip in list_of_finished_trips if trip.is_copy is False]
62
58
 
63
- finished_schedules.append((int(current_trip.ID), db_vehicle_id))
64
- dict_of_events[current_trip.atd] = {
65
- "type": "Trip",
66
- "id": int(current_trip.ID),
67
- }
68
- if i == 0:
69
- raise UnstableSimulationException(
70
- f"New Vehicle required for the rotation/block {current_trip.ID}, which suggests the fleet or the "
71
- f"infrastructure might not be enough for the full electrification. Please add charging "
72
- f"interfaces or increase charging power ."
73
- )
59
+ if len(non_copy_trips) == 0:
60
+ warnings.warn(
61
+ f"Vehicle {db_vehicle_id} has only copied trips and will not be stored in the database. "
62
+ f"This might suggest an unstable simulation result. "
63
+ f"Repetition of this schedule may require more vehicles."
64
+ )
74
65
 
75
- elif i != 0 and i == len(list_of_finished_trips) - 1:
76
- # Vehicle's last trip is a non-copy trip
77
- if earliest_time is None:
78
- earliest_time = list_of_finished_trips[i - 1].ata
79
- latest_time = list_of_finished_trips[i].ata
66
+ return None, None, None
80
67
 
81
- else:
82
- if list_of_finished_trips[i - 1].is_copy is True:
83
- earliest_time = list_of_finished_trips[i - 1].ata
84
- if list_of_finished_trips[i + 1].is_copy is True:
85
- latest_time = list_of_finished_trips[i + 1].atd
68
+ idx_first_orig_trip = list_of_finished_trips.index(non_copy_trips[0])
69
+ idx_last_orig_trip = list_of_finished_trips.index(non_copy_trips[-1])
70
+
71
+ if idx_first_orig_trip == 0:
72
+ unstable_exp.raise_later(non_copy_trips[0])
73
+ earliest_time = non_copy_trips[0].ata
74
+
75
+ else:
76
+ earliest_time = list_of_finished_trips[idx_first_orig_trip - 1].ata
77
+
78
+ if idx_last_orig_trip == len(list_of_finished_trips) - 1:
79
+ # the last trip of this vehicle is a non-copy trip
80
+ all_processes_current_vehicle = non_copy_trips[-1].vehicle.logger.loggedData[
81
+ "dwd.active_processes_copy"
82
+ ]
83
+ time_stamps = [k for k, v in all_processes_current_vehicle.items()]
84
+ last_time_stamp = max(time_stamps)
85
+ if len(all_processes_current_vehicle[last_time_stamp]) == 0:
86
+ latest_time = last_time_stamp
87
+ else:
88
+ current_processes = all_processes_current_vehicle[last_time_stamp]
89
+
90
+ latest_time = 0
91
+ for p in current_processes:
92
+ latest_time = max((p.starts[0] + p.dur), latest_time)
93
+
94
+ else:
95
+ latest_time = list_of_finished_trips[idx_last_orig_trip + 1].atd
96
+
97
+ assert earliest_time <= latest_time
98
+
99
+ finished_schedules = []
100
+ for non_copy_trip in non_copy_trips:
101
+ finished_schedules.append((int(non_copy_trip.ID), db_vehicle_id))
102
+ dict_of_events[non_copy_trip.atd] = {
103
+ "type": "Trip",
104
+ "id": int(non_copy_trip.ID),
105
+ }
86
106
 
87
107
  return finished_schedules, earliest_time, latest_time
88
108
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "eflips-depot"
3
- version = "4.13.0"
3
+ version = "4.13.2"
4
4
  description = "Depot Simulation for eFLIPS"
5
5
  authors = ["Enrico Lauth <enrico.lauth@tu-berlin.de>",
6
6
  "Ludger Heide <ludger.heide@tu-berlin.de",
File without changes
File without changes