eflips-depot 4.10.0__tar.gz → 4.12.0__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.

Potentially problematic release.


This version of eflips-depot might be problematic. Click here for more details.

Files changed (42) hide show
  1. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/PKG-INFO +3 -3
  2. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/api/__init__.py +68 -55
  3. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/api/private/consumption.py +273 -1
  4. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/api/private/results_to_database.py +1 -1
  5. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/api/private/util.py +34 -0
  6. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/depot.py +4 -0
  7. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/pyproject.toml +3 -3
  8. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/LICENSE.md +0 -0
  9. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/README.md +0 -0
  10. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/__init__.py +0 -0
  11. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/api/defaults/default_settings.json +0 -0
  12. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/api/private/__init__.py +0 -0
  13. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/api/private/depot.py +0 -0
  14. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/configuration.py +0 -0
  15. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/evaluation.py +0 -0
  16. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/filters.py +0 -0
  17. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/input_epex_power_price.py +0 -0
  18. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/layout_opt/__init__.py +0 -0
  19. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/layout_opt/doc/__init__.py +0 -0
  20. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/layout_opt/doc/direct_details.pdf +0 -0
  21. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/layout_opt/evaluation.py +0 -0
  22. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/layout_opt/opt_tools/__init__.py +0 -0
  23. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/layout_opt/opt_tools/crossover.py +0 -0
  24. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/layout_opt/opt_tools/fitness_c_urfd.py +0 -0
  25. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/layout_opt/opt_tools/fitness_util.py +0 -0
  26. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/layout_opt/opt_tools/init.py +0 -0
  27. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/layout_opt/opt_tools/mutation.py +0 -0
  28. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/layout_opt/optimize_c_urfd.py +0 -0
  29. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/layout_opt/packing.py +0 -0
  30. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/layout_opt/settings.py +0 -0
  31. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/layout_opt/template_creation.py +0 -0
  32. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/layout_opt/util.py +0 -0
  33. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/plots.py +0 -0
  34. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/processes.py +0 -0
  35. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/rating.py +0 -0
  36. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/resources.py +0 -0
  37. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/settings_config.py +0 -0
  38. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/simple_vehicle.py +0 -0
  39. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/simulation.py +0 -0
  40. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/smart_charging.py +0 -0
  41. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/standalone.py +0 -0
  42. {eflips_depot-4.10.0 → eflips_depot-4.12.0}/eflips/depot/validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: eflips-depot
3
- Version: 4.10.0
3
+ Version: 4.12.0
4
4
  Summary: Depot Simulation for eFLIPS
5
5
  License: AGPL-3.0-or-later
6
6
  Author: Enrico Lauth
@@ -13,8 +13,8 @@ Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Requires-Dist: eflips (>=0.1.3,<0.2.0)
16
- Requires-Dist: eflips-model (>=8.1.0,<9.0.0)
17
- Requires-Dist: eflips-opt (>=0.3.0,<0.4.0)
16
+ Requires-Dist: eflips-model (>8.1.0,<10.0.0)
17
+ Requires-Dist: eflips-opt (>=0.3.3,<0.4.0)
18
18
  Requires-Dist: pandas (>=2.2.0,<3.0.0)
19
19
  Requires-Dist: scipy (>=1.14.0,<2.0.0)
20
20
  Requires-Dist: simpy (>=4.0.1,<5.0.0)
@@ -51,6 +51,7 @@ from eflips.model import (
51
51
  Route,
52
52
  ConsistencyWarning,
53
53
  Station,
54
+ ConsumptionLut,
54
55
  )
55
56
  from sqlalchemy.orm import Session
56
57
 
@@ -59,10 +60,12 @@ from eflips.depot import (
59
60
  DepotEvaluation,
60
61
  SimulationHost,
61
62
  )
63
+ from eflips.depot.api.private.consumption import ConsumptionResult
62
64
  from eflips.depot.api.private.consumption import (
63
65
  initialize_vehicle,
64
66
  add_initial_standby_event,
65
67
  attempt_opportunity_charging_event,
68
+ extract_trip_information,
66
69
  )
67
70
  from eflips.depot.api.private.depot import (
68
71
  delete_depots,
@@ -116,37 +119,42 @@ class SmartChargingStrategy(Enum):
116
119
  """
117
120
 
118
121
 
119
- @dataclass
120
- class ConsumptionResult:
122
+ def generate_consumption_result(scenario):
121
123
  """
122
- A dataclass that stores the results of a charging simulation for a single trip.
123
-
124
- This class holds both the total change in battery State of Charge (SoC) over the trip
125
- as well as an optional timeseries of timestamps and incremental SoC changes. When
126
- an entry exists for a given trip in ``consumption_result``, the simulation will use
127
- these precomputed values instead of recalculating the SoC changes from the vehicle
128
- distance and consumption.
129
-
130
- :param delta_soc_total:
131
- The total change in the vehicle's State of Charge over the trip, typically
132
- negative if the vehicle is consuming energy (e.g., -0.15 means the SoC
133
- dropped by 15%).
134
-
135
- :param timestamps:
136
- A list of timestamps (e.g., arrival times at stops) that mark the times
137
- associated with the SoC changes. The number of timestamps must match the
138
- number of entries in ``delta_soc``.
139
-
140
- :param delta_soc:
141
- A list of cumulative SoC changes corresponding to the ``timestamps``.
142
- For example, if ``delta_soc[i] = -0.02``, it means the SoC decreased by 2%
143
- between from the start of the trip to ``timestamps[i]``. This list should typically
144
- be a monotonic decreasing sequence.
124
+ Generate consumption information for the scenario.
125
+
126
+ This function retrieves the consumption LUT and vehicle classes from the database and returns a dictionary
127
+ containing the consumption information for each vehicle type in the scenario. If a trip has no corresponding
128
+ consumption LUT, it won't be included in the results.
129
+
130
+ :param scenario: A :class:`eflips.model.Scenario` object containing the input data for the simulation.
131
+
132
+ :return: A dictionary containing the consumption information for each vehicle type in the scenario.
145
133
  """
146
134
 
147
- delta_soc_total: float
148
- timestamps: List[datetime] | None
149
- delta_soc: List[float] | None
135
+ with create_session(scenario) as (session, scenario):
136
+ trips = session.query(Trip).filter(Trip.scenario_id == scenario.id).all()
137
+ consumption_results = {}
138
+ for trip in trips:
139
+ try:
140
+ consumption_info = extract_trip_information(
141
+ trip.id,
142
+ scenario,
143
+ )
144
+ except ValueError as e:
145
+ # If the trip has no consumption information, skip it
146
+ logging.warning(
147
+ f"Skipping trip {trip.id} due to missing consumption information: {e}"
148
+ )
149
+ continue
150
+
151
+ battery_capacity_current_vt = trip.rotation.vehicle_type.battery_capacity
152
+ consumption_result = consumption_info.generate_consumption_result(
153
+ battery_capacity_current_vt
154
+ )
155
+ consumption_results[trip.id] = consumption_result
156
+
157
+ return consumption_results
150
158
 
151
159
 
152
160
  def simple_consumption_simulation(
@@ -1103,6 +1111,7 @@ def generate_depot_optimal_size(
1103
1111
  charging_power: float = 90,
1104
1112
  database_url: Optional[str] = None,
1105
1113
  delete_existing_depot: bool = False,
1114
+ use_consumption_lut: bool = False,
1106
1115
  ) -> None:
1107
1116
  """
1108
1117
  Generates an optimal depot layout with the smallest possible size for each depot in the scenario. Line charging areas
@@ -1114,6 +1123,8 @@ def generate_depot_optimal_size(
1114
1123
  :param database_url: An optional database URL. Used if no database url is given by the environment variable.
1115
1124
  :param delete_existing_depot: If there is already a depot existing in this scenario, set True to delete this
1116
1125
  existing depot. Set to False and a ValueError will be raised if there is a depot in this scenario.
1126
+ :param using_consumption_lut: If True, the depot layout will be generated based on the consumption lookup table.
1127
+ If False, constant consumption stored in VehicleType table will be used.
1117
1128
 
1118
1129
  :return: None. The depot layout will be added to the database.
1119
1130
 
@@ -1135,19 +1146,20 @@ def generate_depot_optimal_size(
1135
1146
 
1136
1147
  delete_depots(scenario, session)
1137
1148
 
1138
- # Temporary workaround to set vehicle energy consumption manually
1139
- # TODO: Replace by "use DS consumption if LUT"
1140
- for vehicle_type in (
1141
- session.query(VehicleType)
1142
- .filter(VehicleType.scenario_id == scenario.id)
1143
- .all()
1144
- ):
1145
- vehicle_type.consumption = 2.0
1146
- vehicle_type.vehicle_classes = []
1147
-
1148
1149
  ##### Step 0: Consumption Simulation #####
1149
1150
  # Run the consumption simulation for all depots
1150
- simple_consumption_simulation(scenario, initialize_vehicles=True)
1151
+
1152
+ if use_consumption_lut:
1153
+ # If using the consumption lookup table, we need to calculate the consumption results
1154
+ consumption_results = generate_consumption_result(scenario)
1155
+ simple_consumption_simulation(
1156
+ scenario,
1157
+ initialize_vehicles=True,
1158
+ consumption_result=consumption_results,
1159
+ )
1160
+ else:
1161
+ # If not using the consumption lookup table, we need to initialize the vehicles with the constant consumption
1162
+ simple_consumption_simulation(scenario, initialize_vehicles=True)
1151
1163
 
1152
1164
  ##### Step 1: Find all potential depots #####
1153
1165
  # These are all the spots where a rotation starts and end
@@ -1216,21 +1228,22 @@ def generate_depot_optimal_size(
1216
1228
 
1217
1229
  # create depot with the calculated area sizes
1218
1230
 
1219
- for depot_station, capacities in depot_capacities_for_scenario.items():
1220
- generate_depot(
1221
- capacities,
1222
- depot_station,
1223
- scenario,
1224
- session,
1225
- standard_block_length=standard_block_length,
1226
- charging_power=charging_power,
1227
- num_shunting_slots=num_rotations_for_scenario[depot_station] // 10,
1228
- num_cleaning_slots=num_rotations_for_scenario[depot_station] // 10,
1229
- )
1231
+ with create_session(scenario, database_url) as (session, scenario):
1232
+ for depot_station, capacities in depot_capacities_for_scenario.items():
1233
+ generate_depot(
1234
+ capacities,
1235
+ depot_station,
1236
+ scenario,
1237
+ session,
1238
+ standard_block_length=standard_block_length,
1239
+ charging_power=charging_power,
1240
+ num_shunting_slots=num_rotations_for_scenario[depot_station] // 10,
1241
+ num_cleaning_slots=num_rotations_for_scenario[depot_station] // 10,
1242
+ )
1230
1243
 
1231
- # Delete all vehicles and events again. Only depot layout is kept
1244
+ # Delete all vehicles and events again. Only depot layout is kept
1232
1245
 
1233
- rotation_q = session.query(Rotation).filter(Rotation.scenario_id == scenario.id)
1234
- rotation_q.update({"vehicle_id": None})
1235
- session.query(Event).filter(Event.scenario_id == scenario.id).delete()
1236
- session.query(Vehicle).filter(Vehicle.scenario_id == scenario.id).delete()
1246
+ rotation_q = session.query(Rotation).filter(Rotation.scenario_id == scenario.id)
1247
+ rotation_q.update({"vehicle_id": None})
1248
+ session.query(Event).filter(Event.scenario_id == scenario.id).delete()
1249
+ session.query(Vehicle).filter(Vehicle.scenario_id == scenario.id).delete()
@@ -1,9 +1,11 @@
1
1
  import logging
2
2
  import warnings
3
+ from dataclasses import dataclass
3
4
  from datetime import timedelta, datetime
4
5
  from math import ceil
5
- from typing import Tuple
6
+ from typing import Tuple, List
6
7
  from zoneinfo import ZoneInfo
8
+ import scipy
7
9
 
8
10
  import numpy as np
9
11
  import sqlalchemy.orm
@@ -12,11 +14,281 @@ from eflips.model import (
12
14
  EventType,
13
15
  Rotation,
14
16
  Vehicle,
17
+ VehicleType,
18
+ VehicleClass,
15
19
  Trip,
16
20
  Station,
17
21
  ChargeType,
18
22
  ConsistencyWarning,
23
+ ConsumptionLut,
24
+ Scenario,
19
25
  )
26
+ from sqlalchemy.orm import joinedload
27
+
28
+ from eflips.depot.api.private.util import temperature_for_trip, create_session
29
+
30
+
31
+ @dataclass
32
+ class ConsumptionResult:
33
+ """
34
+ A dataclass that stores the results of a charging simulation for a single trip.
35
+
36
+ This class holds both the total change in battery State of Charge (SoC) over the trip
37
+ as well as an optional timeseries of timestamps and incremental SoC changes. When
38
+ an entry exists for a given trip in ``consumption_result``, the simulation will use
39
+ these precomputed values instead of recalculating the SoC changes from the vehicle
40
+ distance and consumption.
41
+
42
+ :param delta_soc_total:
43
+ The total change in the vehicle's State of Charge over the trip, typically
44
+ negative if the vehicle is consuming energy (e.g., -0.15 means the SoC
45
+ dropped by 15%).
46
+
47
+ :param timestamps:
48
+ A list of timestamps (e.g., arrival times at stops) that mark the times
49
+ associated with the SoC changes. The number of timestamps must match the
50
+ number of entries in ``delta_soc``.
51
+
52
+ :param delta_soc:
53
+ A list of cumulative SoC changes corresponding to the ``timestamps``.
54
+ For example, if ``delta_soc[i] = -0.02``, it means the SoC decreased by 2%
55
+ between from the start of the trip to ``timestamps[i]``. This list should typically
56
+ be a monotonic decreasing sequence.
57
+ """
58
+
59
+ delta_soc_total: float
60
+ timestamps: List[datetime] | None
61
+ delta_soc: List[float] | None
62
+
63
+
64
+ @dataclass
65
+ class ConsumptionInformation:
66
+ """
67
+ A dataclass to hold the information needed for the consumption simulation.
68
+
69
+ :param trip_id:
70
+ The ID of the trip for which the consumption is calculated.
71
+ :param consumption_lut:
72
+ The ConsumptionLut object for the vehicle class. This is used to calculate the
73
+ consumption based on the trip parameters.
74
+ :param average_speed:
75
+ The average speed of the trip in km/h. This is used to calculate the consumption.
76
+ :param distance:
77
+ The distance of the trip in km. This is used to calculate the total consumption.
78
+ :param temperature:
79
+ The ambient temperature in °C. This is used to calculate the consumption.
80
+ :param level_of_loading:
81
+ The level of loading of the vehicle as a fraction of its maximum payload.
82
+ :param incline:
83
+ The incline of the trip as a fraction (0.0-1.0). This is used to calculate the consumption.
84
+ :param consumption:
85
+ The total consumption of the trip in kWh. This is calculated based on the LUT and trip parameters.
86
+ :param consumption_per_km:
87
+ The consumption per km in kWh. This is calculated based on the LUT and trip parameters.
88
+ """
89
+
90
+ trip_id: int
91
+ consumption_lut: ConsumptionLut | None # the LUT for the vehicle class
92
+ average_speed: float # the average speed of the trip in km/h
93
+ distance: float # the distance of the trip in km
94
+ temperature: float # The ambient temperature in °C
95
+ level_of_loading: float
96
+ incline: float = 0.0 # The incline of the trip in 0.0-1.0
97
+ consumption: float = None # The consumption of the trip in kWh
98
+ consumption_per_km: float = None # The consumption per km in kWh
99
+
100
+ def calculate(self):
101
+ """
102
+ Calculates the consumption for the trip. Returns a float in kWh.
103
+
104
+ :return: The energy consumption in kWh. This is already the consumption for the whole trip.
105
+ """
106
+
107
+ # Make sure the consumption lut has 4 dimensions and the columns are in the correct order
108
+ if self.consumption_lut.columns != [
109
+ "incline",
110
+ "t_amb",
111
+ "level_of_loading",
112
+ "mean_speed_kmh",
113
+ ]:
114
+ raise ValueError(
115
+ "The consumption LUT must have the columns 'incline', 't_amb', 'level_of_loading', 'mean_speed_kmh'"
116
+ )
117
+
118
+ # Recover the scales along each of the four axes from the datapoints
119
+ incline_scale = sorted(set([x[0] for x in self.consumption_lut.data_points]))
120
+ temperature_scale = sorted(
121
+ set([x[1] for x in self.consumption_lut.data_points])
122
+ )
123
+ level_of_loading_scale = sorted(
124
+ set([x[2] for x in self.consumption_lut.data_points])
125
+ )
126
+ speed_scale = sorted(set([x[3] for x in self.consumption_lut.data_points]))
127
+
128
+ # Create the 4d array
129
+ consumption_lut = np.zeros(
130
+ (
131
+ len(incline_scale),
132
+ len(temperature_scale),
133
+ len(level_of_loading_scale),
134
+ len(speed_scale),
135
+ )
136
+ )
137
+
138
+ # Fill it with NaNs
139
+ consumption_lut.fill(np.nan)
140
+
141
+ for i, (incline, temperature, level_of_loading, speed) in enumerate(
142
+ self.consumption_lut.data_points
143
+ ):
144
+ consumption_lut[
145
+ incline_scale.index(incline),
146
+ temperature_scale.index(temperature),
147
+ level_of_loading_scale.index(level_of_loading),
148
+ speed_scale.index(speed),
149
+ ] = self.consumption_lut.values[i]
150
+
151
+ # Interpolate the consumption
152
+ interpolator = scipy.interpolate.RegularGridInterpolator(
153
+ (incline_scale, temperature_scale, level_of_loading_scale, speed_scale),
154
+ consumption_lut,
155
+ bounds_error=False,
156
+ fill_value=None,
157
+ method="linear",
158
+ )
159
+ consumption_per_km = interpolator(
160
+ [self.incline, self.temperature, self.level_of_loading, self.average_speed]
161
+ )[0]
162
+
163
+ # This is a temporary workaround to handle cases where the LUT does not contain
164
+ if consumption_per_km is None or np.isnan(consumption_per_km):
165
+ # Add a warning if we had to use nearest neighbor interpolation
166
+ warnings.warn(
167
+ f"Consumption LUT for trip {self.trip_id} with parameters: "
168
+ f"incline={self.incline}, temperature={self.temperature}, "
169
+ f"level_of_loading={self.level_of_loading}, average_speed={self.average_speed} "
170
+ f"returned NaN. Using nearest neighbor interpolation instead. The result may be less accurate.",
171
+ ConsistencyWarning,
172
+ )
173
+
174
+ interpolator_nn = scipy.interpolate.RegularGridInterpolator(
175
+ (incline_scale, temperature_scale, level_of_loading_scale, speed_scale),
176
+ consumption_lut,
177
+ bounds_error=False,
178
+ fill_value=None, # Fill NaN with 0.0
179
+ method="nearest",
180
+ )
181
+ consumption_per_km = interpolator_nn(
182
+ [
183
+ self.incline,
184
+ self.temperature,
185
+ self.level_of_loading,
186
+ self.average_speed,
187
+ ]
188
+ )[0]
189
+
190
+ # Add a warning if we had to use nearest neighbor interpolation
191
+
192
+ if consumption_per_km is None or np.isnan(consumption_per_km):
193
+ raise ValueError(
194
+ f"Could not calculate consumption for trip {self.trip_id} with parameters: "
195
+ f"incline={self.incline}, temperature={self.temperature}, "
196
+ f"level_of_loading={self.level_of_loading}, average_speed={self.average_speed}. "
197
+ f"Possible reason: data points missing in the LUT."
198
+ )
199
+
200
+ self.consumption = consumption_per_km * self.distance
201
+ self.consumption_per_km = consumption_per_km
202
+ self.consumption_lut = None # To save memory
203
+
204
+ def generate_consumption_result(self, battery_capacity) -> ConsumptionResult:
205
+ """
206
+ Generates a ConsumptionResult object from the current instance.
207
+
208
+ :param battery_capacity: The battery capacity in kWh.
209
+ :return: A ConsumptionResult object containing the total change in SoC and optional timeseries.
210
+ """
211
+ if self.consumption is None:
212
+ raise ValueError(
213
+ "Consumption must be calculated before generating a result."
214
+ )
215
+
216
+ # TODO implement a timeseries of timestamps and delta_soc
217
+ consumption_result = ConsumptionResult(
218
+ delta_soc_total=-float(self.consumption) / battery_capacity,
219
+ timestamps=None,
220
+ delta_soc=None,
221
+ )
222
+ return consumption_result
223
+
224
+
225
+ def extract_trip_information(
226
+ trip_id: int,
227
+ scenario: Scenario,
228
+ passenger_mass=68,
229
+ passenger_count=17.6,
230
+ ) -> ConsumptionInformation:
231
+ """
232
+ Extracts the information needed for the consumption simulation from a trip.
233
+ """
234
+
235
+ with create_session(scenario) as (session, scenario):
236
+ # Load the trip with its route and rotation, including vehicle type and consumption LUT
237
+ # We use joinedload to avoid N+1 queries
238
+
239
+ trip = (
240
+ session.query(Trip)
241
+ .filter(Trip.id == trip_id)
242
+ .options(joinedload(Trip.route))
243
+ .options(
244
+ joinedload(Trip.rotation)
245
+ .joinedload(Rotation.vehicle_type)
246
+ .joinedload(VehicleType.vehicle_classes)
247
+ .joinedload(VehicleClass.consumption_lut)
248
+ )
249
+ .one()
250
+ )
251
+ # Check exactly one of the vehicle classes has a consumption LUT
252
+ all_consumption_luts = [
253
+ vehicle_class.consumption_lut
254
+ for vehicle_class in trip.rotation.vehicle_type.vehicle_classes
255
+ ]
256
+ all_consumption_luts = [x for x in all_consumption_luts if x is not None]
257
+ if len(all_consumption_luts) != 1:
258
+ raise ValueError(
259
+ f"Expected exactly one consumption LUT, got {len(all_consumption_luts)}"
260
+ )
261
+ consumption_lut = all_consumption_luts[0]
262
+ # Disconnect the consumption LUT from the session to avoid loading the whole table
263
+
264
+ del all_consumption_luts
265
+
266
+ total_distance = trip.route.distance / 1000 # km
267
+ total_duration = (
268
+ trip.arrival_time - trip.departure_time
269
+ ).total_seconds() / 3600
270
+ average_speed = total_distance / total_duration # km/h
271
+
272
+ temperature = temperature_for_trip(trip_id, session)
273
+
274
+ payload_mass = passenger_mass * passenger_count
275
+ full_payload = (
276
+ trip.rotation.vehicle_type.allowed_mass
277
+ - trip.rotation.vehicle_type.empty_mass
278
+ )
279
+ level_of_loading = payload_mass / full_payload
280
+
281
+ info = ConsumptionInformation(
282
+ trip_id=trip.id,
283
+ consumption_lut=consumption_lut,
284
+ average_speed=average_speed,
285
+ distance=total_distance,
286
+ temperature=temperature,
287
+ level_of_loading=level_of_loading,
288
+ )
289
+
290
+ info.calculate()
291
+ return info
20
292
 
21
293
 
22
294
  def initialize_vehicle(rotation: Rotation, session: sqlalchemy.orm.session.Session):
@@ -67,7 +67,7 @@ def get_finished_schedules_per_vehicle(
67
67
  }
68
68
  if i == 0:
69
69
  raise UnstableSimulationException(
70
- f"New Vehicle required for the trip {current_trip.ID}, which suggests the fleet or the "
70
+ f"New Vehicle required for the rotation/block {current_trip.ID}, which suggests the fleet or the "
71
71
  f"infrastructure might not be enough for the full electrification. Please add charging "
72
72
  f"interfaces or increase charging power ."
73
73
  )
@@ -7,6 +7,7 @@ from datetime import timedelta, datetime
7
7
  from typing import Union, Any, Optional, Tuple, Dict, List
8
8
 
9
9
  import simpy
10
+ import numpy as np
10
11
  from eflips.model import (
11
12
  Scenario,
12
13
  VehicleType,
@@ -15,6 +16,7 @@ from eflips.model import (
15
16
  EventType,
16
17
  Trip,
17
18
  Depot,
19
+ Temperatures,
18
20
  )
19
21
  from sqlalchemy import inspect, create_engine
20
22
  from sqlalchemy.orm import Session
@@ -199,6 +201,38 @@ def check_depot_validity(depot: Depot) -> None:
199
201
  ), "All processes except the last one must have electric power."
200
202
 
201
203
 
204
+ def temperature_for_trip(trip_id: int, session: Session) -> float:
205
+ """
206
+ Returns the temperature for a trip. Finds the temperature for the mid-point of the trip.
207
+
208
+ :param trip_id: The ID of the trip
209
+ :param session: The SQLAlchemy session
210
+ :return: A temperature in °C
211
+ """
212
+
213
+ trip = session.query(Trip).filter(Trip.id == trip_id).one()
214
+ temperatures = (
215
+ session.query(Temperatures)
216
+ .filter(Temperatures.scenario_id == trip.scenario_id)
217
+ .one()
218
+ )
219
+
220
+ # Find the mid-point of the trip
221
+ mid_time = trip.departure_time + (trip.arrival_time - trip.departure_time) / 2
222
+
223
+ if temperatures.use_only_time:
224
+ # The temperatures are only given by time. We change our mid-time to be the date of the temperatures
225
+ mid_time = datetime.combine(temperatures.datetimes[0].date(), mid_time.time())
226
+
227
+ mid_time = mid_time.timestamp()
228
+
229
+ datetimes = [dt.timestamp() for dt in temperatures.datetimes]
230
+ temperatures = temperatures.data
231
+
232
+ temperature = np.interp(mid_time, datetimes, temperatures)
233
+ return float(temperature)
234
+
235
+
202
236
  @dataclass
203
237
  class VehicleSchedule:
204
238
  """
@@ -1514,6 +1514,10 @@ class DepotControl:
1514
1514
 
1515
1515
  def schedule_for_matching(self, trip):
1516
1516
  """Wait *delay* and then schedule *trip* for matching with a vehicle."""
1517
+
1518
+ # Shuyao: This delay does not mean the trip is delayed. It means the trip must be scheduled one hour before departure
1519
+ # so the time of the environment is "delayed" until 1 hour before departure.
1520
+
1517
1521
  delay = self.dispatch_strategy.scheduling_delay(self.env, trip)
1518
1522
  yield self.env.timeout(delay)
1519
1523
  self.depot.unassigned_trips.append(trip)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "eflips-depot"
3
- version = "4.10.0"
3
+ version = "4.12.0"
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",
@@ -22,8 +22,8 @@ xlsxwriter = "^3.1.9"
22
22
  pandas = "^2.2.0"
23
23
  xlrd = "<=1.2.0"
24
24
  scipy = "^1.14.0"
25
- eflips-model = "^8.1.0"
26
- eflips-opt = "^0.3.0"
25
+ eflips-model = ">8.1.0, <10.0.0"
26
+ eflips-opt = "^0.3.3"
27
27
 
28
28
 
29
29
  [tool.pytest.ini_options]
File without changes
File without changes