eflips-depot 4.10.0__py3-none-any.whl → 4.12.0__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 +68 -55
- eflips/depot/api/private/consumption.py +273 -1
- eflips/depot/api/private/results_to_database.py +1 -1
- eflips/depot/api/private/util.py +34 -0
- eflips/depot/depot.py +4 -0
- {eflips_depot-4.10.0.dist-info → eflips_depot-4.12.0.dist-info}/METADATA +3 -3
- {eflips_depot-4.10.0.dist-info → eflips_depot-4.12.0.dist-info}/RECORD +9 -9
- {eflips_depot-4.10.0.dist-info → eflips_depot-4.12.0.dist-info}/LICENSE.md +0 -0
- {eflips_depot-4.10.0.dist-info → eflips_depot-4.12.0.dist-info}/WHEEL +0 -0
eflips/depot/api/__init__.py
CHANGED
|
@@ -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
|
-
|
|
120
|
-
class ConsumptionResult:
|
|
122
|
+
def generate_consumption_result(scenario):
|
|
121
123
|
"""
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
This
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
:
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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
|
-
|
|
1244
|
+
# Delete all vehicles and events again. Only depot layout is kept
|
|
1232
1245
|
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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
|
|
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
|
)
|
eflips/depot/api/private/util.py
CHANGED
|
@@ -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
|
"""
|
eflips/depot/depot.py
CHANGED
|
@@ -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
|
Metadata-Version: 2.3
|
|
2
2
|
Name: eflips-depot
|
|
3
|
-
Version: 4.
|
|
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 (
|
|
17
|
-
Requires-Dist: eflips-opt (>=0.3.
|
|
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)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
eflips/depot/__init__.py,sha256=06GUem0JIEunIyJ0_P_MLAGfibGEnNqcPPY0OBpO2NQ,1662
|
|
2
|
-
eflips/depot/api/__init__.py,sha256=
|
|
2
|
+
eflips/depot/api/__init__.py,sha256=t153i_ZJA82O5w7GSY3-NAz8sSDI4mLZdjKrroFu_-M,54945
|
|
3
3
|
eflips/depot/api/defaults/default_settings.json,sha256=0eUDTw_rtLQFvthP8oJL93iRXlmAOravAg-4qqGMQAY,5375
|
|
4
4
|
eflips/depot/api/private/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
eflips/depot/api/private/consumption.py,sha256=
|
|
5
|
+
eflips/depot/api/private/consumption.py,sha256=nImegyhKrZlEwKTNdmAmDxjTQCvwfPgxA588e0M_F9o,27282
|
|
6
6
|
eflips/depot/api/private/depot.py,sha256=-LpLeEi3ctjsLzWbBGDBTic_aKrb6S35ii5wt-7QTmI,43359
|
|
7
|
-
eflips/depot/api/private/results_to_database.py,sha256=
|
|
8
|
-
eflips/depot/api/private/util.py,sha256=
|
|
7
|
+
eflips/depot/api/private/results_to_database.py,sha256=_qXB9046dWHek4ys669J3IojJt4xlZRkW1HxT7OTLFo,25524
|
|
8
|
+
eflips/depot/api/private/util.py,sha256=rDqVuB4sllFZmNvXuDktIsFRoR9l-AWc4QM00e1Quvw,17966
|
|
9
9
|
eflips/depot/configuration.py,sha256=Op3hlir-dEN7yHr0kTqbYANoCBKFWK6uKOv3NJl8w_w,35678
|
|
10
|
-
eflips/depot/depot.py,sha256=
|
|
10
|
+
eflips/depot/depot.py,sha256=pREutXtJlDxjgfwRobAy7UqTHh-tldbVWHN8DIyxs8s,106986
|
|
11
11
|
eflips/depot/evaluation.py,sha256=qqXyP4jA1zFcKuWhliQ6n25ZlGl9mJV-vtXf0yu8WN8,140842
|
|
12
12
|
eflips/depot/filters.py,sha256=1aUK7czuhiATC3P3NN5oRtH1I-kN_-mp_-vkzyyBXn4,16089
|
|
13
13
|
eflips/depot/input_epex_power_price.py,sha256=VPDC1zy-klQpveGIZ8941hL1P_jeNq3IHoLgFTsANig,5569
|
|
@@ -36,7 +36,7 @@ eflips/depot/simulation.py,sha256=ee0qTzOzG-8ybN36ie_NJallXfC7jUaS9JZvaYFziLs,10
|
|
|
36
36
|
eflips/depot/smart_charging.py,sha256=C3BYqzn2-OYY4ipXm0ETtavbAM9QXZMYULBpVoChf0E,54311
|
|
37
37
|
eflips/depot/standalone.py,sha256=8O01zEXghFG9zZBu0fUD0sXvbHQ-AXw6RB5M750a_sM,22419
|
|
38
38
|
eflips/depot/validation.py,sha256=TIuY7cQtEJI4H2VVMSuY5IIVkacEEZ67weeMuY3NSAM,7097
|
|
39
|
-
eflips_depot-4.
|
|
40
|
-
eflips_depot-4.
|
|
41
|
-
eflips_depot-4.
|
|
42
|
-
eflips_depot-4.
|
|
39
|
+
eflips_depot-4.12.0.dist-info/LICENSE.md,sha256=KB4XTk1fPHjtZCYDyPyreu6h1LVJVZXYg-5vePcWZAc,34143
|
|
40
|
+
eflips_depot-4.12.0.dist-info/METADATA,sha256=IhvhXaJwVlfImiFwOdf4RTAH7rTmMnpabfsogv18mO8,5985
|
|
41
|
+
eflips_depot-4.12.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
42
|
+
eflips_depot-4.12.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|