eflips-depot 4.9.1__tar.gz → 4.11.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.
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/PKG-INFO +1 -1
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/api/__init__.py +170 -38
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/api/private/consumption.py +273 -1
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/api/private/depot.py +2 -1
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/api/private/results_to_database.py +1 -1
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/api/private/util.py +34 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/depot.py +4 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/pyproject.toml +1 -1
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/LICENSE.md +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/README.md +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/__init__.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/api/defaults/default_settings.json +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/api/private/__init__.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/configuration.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/evaluation.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/filters.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/input_epex_power_price.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/layout_opt/__init__.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/layout_opt/doc/__init__.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/layout_opt/doc/direct_details.pdf +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/layout_opt/evaluation.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/layout_opt/opt_tools/__init__.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/layout_opt/opt_tools/crossover.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/layout_opt/opt_tools/fitness_c_urfd.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/layout_opt/opt_tools/fitness_util.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/layout_opt/opt_tools/init.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/layout_opt/opt_tools/mutation.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/layout_opt/optimize_c_urfd.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/layout_opt/packing.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/layout_opt/settings.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/layout_opt/template_creation.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/layout_opt/util.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/plots.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/processes.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/rating.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/resources.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/settings_config.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/simple_vehicle.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/simulation.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/smart_charging.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/standalone.py +0 -0
- {eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/validation.py +0 -0
|
@@ -49,6 +49,9 @@ from eflips.model import (
|
|
|
49
49
|
AreaType,
|
|
50
50
|
ChargeType,
|
|
51
51
|
Route,
|
|
52
|
+
ConsistencyWarning,
|
|
53
|
+
Station,
|
|
54
|
+
ConsumptionLut,
|
|
52
55
|
)
|
|
53
56
|
from sqlalchemy.orm import Session
|
|
54
57
|
|
|
@@ -57,16 +60,19 @@ from eflips.depot import (
|
|
|
57
60
|
DepotEvaluation,
|
|
58
61
|
SimulationHost,
|
|
59
62
|
)
|
|
63
|
+
from eflips.depot.api.private.consumption import ConsumptionResult
|
|
60
64
|
from eflips.depot.api.private.consumption import (
|
|
61
65
|
initialize_vehicle,
|
|
62
66
|
add_initial_standby_event,
|
|
63
67
|
attempt_opportunity_charging_event,
|
|
68
|
+
extract_trip_information,
|
|
64
69
|
)
|
|
65
70
|
from eflips.depot.api.private.depot import (
|
|
66
71
|
delete_depots,
|
|
67
72
|
depot_to_template,
|
|
68
73
|
group_rotations_by_start_end_stop,
|
|
69
74
|
generate_depot,
|
|
75
|
+
depot_smallest_possible_size,
|
|
70
76
|
)
|
|
71
77
|
from eflips.depot.api.private.results_to_database import (
|
|
72
78
|
get_finished_schedules_per_vehicle,
|
|
@@ -113,37 +119,33 @@ class SmartChargingStrategy(Enum):
|
|
|
113
119
|
"""
|
|
114
120
|
|
|
115
121
|
|
|
116
|
-
|
|
117
|
-
class ConsumptionResult:
|
|
122
|
+
def generate_consumption_result(scenario):
|
|
118
123
|
"""
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
This
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
:param delta_soc_total:
|
|
128
|
-
The total change in the vehicle's State of Charge over the trip, typically
|
|
129
|
-
negative if the vehicle is consuming energy (e.g., -0.15 means the SoC
|
|
130
|
-
dropped by 15%).
|
|
131
|
-
|
|
132
|
-
:param timestamps:
|
|
133
|
-
A list of timestamps (e.g., arrival times at stops) that mark the times
|
|
134
|
-
associated with the SoC changes. The number of timestamps must match the
|
|
135
|
-
number of entries in ``delta_soc``.
|
|
136
|
-
|
|
137
|
-
:param delta_soc:
|
|
138
|
-
A list of cumulative SoC changes corresponding to the ``timestamps``.
|
|
139
|
-
For example, if ``delta_soc[i] = -0.02``, it means the SoC decreased by 2%
|
|
140
|
-
between from the start of the trip to ``timestamps[i]``. This list should typically
|
|
141
|
-
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.
|
|
128
|
+
|
|
129
|
+
:param scenario: A :class:`eflips.model.Scenario` object containing the input data for the simulation.
|
|
130
|
+
|
|
131
|
+
:return: A dictionary containing the consumption information for each vehicle type in the scenario.
|
|
142
132
|
"""
|
|
143
133
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
134
|
+
with create_session(scenario) as (session, scenario):
|
|
135
|
+
trips = session.query(Trip).filter(Trip.scenario_id == scenario.id).all()
|
|
136
|
+
consumption_results = {}
|
|
137
|
+
for trip in trips:
|
|
138
|
+
consumption_info = extract_trip_information(
|
|
139
|
+
trip.id,
|
|
140
|
+
scenario,
|
|
141
|
+
)
|
|
142
|
+
battery_capacity_current_vt = trip.rotation.vehicle_type.battery_capacity
|
|
143
|
+
consumption_result = consumption_info.generate_consumption_result(
|
|
144
|
+
battery_capacity_current_vt
|
|
145
|
+
)
|
|
146
|
+
consumption_results[trip.id] = consumption_result
|
|
147
|
+
|
|
148
|
+
return consumption_results
|
|
147
149
|
|
|
148
150
|
|
|
149
151
|
def simple_consumption_simulation(
|
|
@@ -492,19 +494,14 @@ def generate_depot_layout(
|
|
|
492
494
|
|
|
493
495
|
# Create one direct slot for each rotation (it's way too much, but should work)
|
|
494
496
|
vt_capacity_dict: Dict[VehicleType, Dict[AreaType, None | int]] = {}
|
|
497
|
+
rotation_count_depot = 0
|
|
495
498
|
for vehicle_type, rotations in vehicle_type_dict.items():
|
|
496
|
-
rotation_count = len(rotations)
|
|
497
499
|
vt_capacity_dict[vehicle_type] = {
|
|
498
500
|
AreaType.LINE: None,
|
|
499
|
-
AreaType.DIRECT_ONESIDE:
|
|
501
|
+
AreaType.DIRECT_ONESIDE: len(rotations),
|
|
500
502
|
AreaType.DIRECT_TWOSIDE: None,
|
|
501
503
|
}
|
|
502
|
-
|
|
503
|
-
total_rotation_count = (
|
|
504
|
-
session.query(Rotation)
|
|
505
|
-
.filter(Rotation.scenario_id == scenario.id)
|
|
506
|
-
.count()
|
|
507
|
-
)
|
|
504
|
+
rotation_count_depot += len(rotations)
|
|
508
505
|
|
|
509
506
|
generate_depot(
|
|
510
507
|
vt_capacity_dict,
|
|
@@ -512,8 +509,8 @@ def generate_depot_layout(
|
|
|
512
509
|
scenario,
|
|
513
510
|
session,
|
|
514
511
|
charging_power=charging_power,
|
|
515
|
-
num_shunting_slots=max(
|
|
516
|
-
num_cleaning_slots=max(
|
|
512
|
+
num_shunting_slots=max(rotation_count_depot // 10, 1),
|
|
513
|
+
num_cleaning_slots=max(rotation_count_depot // 10, 1),
|
|
517
514
|
)
|
|
518
515
|
|
|
519
516
|
|
|
@@ -1097,3 +1094,138 @@ def add_evaluation_to_database(
|
|
|
1097
1094
|
# Postprocessing of events
|
|
1098
1095
|
update_vehicle_in_rotation(session, scenario, list_of_assigned_schedules)
|
|
1099
1096
|
update_waiting_events(session, scenario, waiting_area_id)
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
def generate_depot_optimal_size(
|
|
1100
|
+
scenario: Scenario,
|
|
1101
|
+
standard_block_length: int = 6,
|
|
1102
|
+
charging_power: float = 90,
|
|
1103
|
+
database_url: Optional[str] = None,
|
|
1104
|
+
delete_existing_depot: bool = False,
|
|
1105
|
+
consumption_results: Optional[Dict[int, ConsumptionResult]] = None,
|
|
1106
|
+
) -> None:
|
|
1107
|
+
"""
|
|
1108
|
+
Generates an optimal depot layout with the smallest possible size for each depot in the scenario. Line charging areas
|
|
1109
|
+
with given block length area preferred. The existing depot will be deleted if `delete_existing_depot` is set to True.
|
|
1110
|
+
|
|
1111
|
+
:param scenario: A :class:`eflips.model.Scenario` object containing the input data for the simulation.
|
|
1112
|
+
:param standard_block_length: The standard block length for the depot layout in meters. Default is 6.
|
|
1113
|
+
:param charging_power: The charging power of the charging area in kW. Default is 90.
|
|
1114
|
+
:param database_url: An optional database URL. Used if no database url is given by the environment variable.
|
|
1115
|
+
:param delete_existing_depot: If there is already a depot existing in this scenario, set True to delete this
|
|
1116
|
+
existing depot. Set to False and a ValueError will be raised if there is a depot in this scenario.
|
|
1117
|
+
:param consumption_results: A dictionary of consumption results for each vehicle type. It is used to simulate the consumption with
|
|
1118
|
+
given look-up tables. If not given, the constant consumption will be used in the simulation.
|
|
1119
|
+
|
|
1120
|
+
:return: None. The depot layout will be added to the database.
|
|
1121
|
+
|
|
1122
|
+
"""
|
|
1123
|
+
|
|
1124
|
+
logger = logging.getLogger(__name__)
|
|
1125
|
+
|
|
1126
|
+
with create_session(scenario, database_url) as (session, scenario):
|
|
1127
|
+
# Delete all vehicles and events, also disconnect the vehicles from the rotations
|
|
1128
|
+
rotation_q = session.query(Rotation).filter(Rotation.scenario_id == scenario.id)
|
|
1129
|
+
rotation_q.update({"vehicle_id": None})
|
|
1130
|
+
session.query(Event).filter(Event.scenario_id == scenario.id).delete()
|
|
1131
|
+
session.query(Vehicle).filter(Vehicle.scenario_id == scenario.id).delete()
|
|
1132
|
+
|
|
1133
|
+
# Handles existing depot
|
|
1134
|
+
if session.query(Depot).filter(Depot.scenario_id == scenario.id).count() != 0:
|
|
1135
|
+
if delete_existing_depot is False:
|
|
1136
|
+
raise ValueError("Depot already exists.")
|
|
1137
|
+
|
|
1138
|
+
delete_depots(scenario, session)
|
|
1139
|
+
|
|
1140
|
+
##### Step 0: Consumption Simulation #####
|
|
1141
|
+
# Run the consumption simulation for all depots
|
|
1142
|
+
simple_consumption_simulation(
|
|
1143
|
+
scenario, initialize_vehicles=True, consumption_result=consumption_results
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1146
|
+
##### Step 1: Find all potential depots #####
|
|
1147
|
+
# These are all the spots where a rotation starts and end
|
|
1148
|
+
warnings.simplefilter("ignore", category=ConsistencyWarning)
|
|
1149
|
+
warnings.simplefilter("ignore", category=UserWarning)
|
|
1150
|
+
|
|
1151
|
+
depot_capacities_for_scenario: Dict[
|
|
1152
|
+
Station, Dict[VehicleType, Dict[AreaType, int]]
|
|
1153
|
+
] = {}
|
|
1154
|
+
|
|
1155
|
+
num_rotations_for_scenario: Dict[Station, int] = {}
|
|
1156
|
+
|
|
1157
|
+
for (
|
|
1158
|
+
first_last_stop_tup,
|
|
1159
|
+
vehicle_type_dict,
|
|
1160
|
+
) in group_rotations_by_start_end_stop(scenario.id, session).items():
|
|
1161
|
+
first_stop, last_stop = first_last_stop_tup
|
|
1162
|
+
if first_stop != last_stop:
|
|
1163
|
+
raise ValueError("First and last stop of a rotation are not the same.")
|
|
1164
|
+
|
|
1165
|
+
station = first_stop
|
|
1166
|
+
rotation_count_depot = sum(
|
|
1167
|
+
len(rotations) for vehicle_type, rotations in vehicle_type_dict.items()
|
|
1168
|
+
)
|
|
1169
|
+
|
|
1170
|
+
savepoint = session.begin_nested()
|
|
1171
|
+
try:
|
|
1172
|
+
# (Temporarily) Delete all rotations not starting or ending at the station
|
|
1173
|
+
logger.debug(
|
|
1174
|
+
f"Deleting all rotations not starting or ending at {station.name}"
|
|
1175
|
+
)
|
|
1176
|
+
all_rot_for_scenario = (
|
|
1177
|
+
session.query(Rotation)
|
|
1178
|
+
.filter(Rotation.scenario_id == scenario.id)
|
|
1179
|
+
.all()
|
|
1180
|
+
)
|
|
1181
|
+
to_delete = []
|
|
1182
|
+
for rot in all_rot_for_scenario:
|
|
1183
|
+
first_stop = rot.trips[0].route.departure_station
|
|
1184
|
+
if first_stop != station:
|
|
1185
|
+
for trip in rot.trips:
|
|
1186
|
+
for stop_time in trip.stop_times:
|
|
1187
|
+
to_delete.append(stop_time)
|
|
1188
|
+
for event in trip.events:
|
|
1189
|
+
to_delete.append(event)
|
|
1190
|
+
to_delete.append(trip)
|
|
1191
|
+
to_delete.append(rot)
|
|
1192
|
+
for obj in to_delete:
|
|
1193
|
+
session.flush()
|
|
1194
|
+
session.delete(obj)
|
|
1195
|
+
session.flush()
|
|
1196
|
+
|
|
1197
|
+
logger.info(f"Generating depot layout for station {station.name}")
|
|
1198
|
+
vt_capacities_for_station = depot_smallest_possible_size(
|
|
1199
|
+
station,
|
|
1200
|
+
scenario,
|
|
1201
|
+
session,
|
|
1202
|
+
standard_block_length,
|
|
1203
|
+
charging_power,
|
|
1204
|
+
)
|
|
1205
|
+
|
|
1206
|
+
depot_capacities_for_scenario[station] = vt_capacities_for_station
|
|
1207
|
+
num_rotations_for_scenario[station] = rotation_count_depot
|
|
1208
|
+
finally:
|
|
1209
|
+
savepoint.rollback()
|
|
1210
|
+
|
|
1211
|
+
# create depot with the calculated area sizes
|
|
1212
|
+
|
|
1213
|
+
with create_session(scenario, database_url) as (session, scenario):
|
|
1214
|
+
for depot_station, capacities in depot_capacities_for_scenario.items():
|
|
1215
|
+
generate_depot(
|
|
1216
|
+
capacities,
|
|
1217
|
+
depot_station,
|
|
1218
|
+
scenario,
|
|
1219
|
+
session,
|
|
1220
|
+
standard_block_length=standard_block_length,
|
|
1221
|
+
charging_power=charging_power,
|
|
1222
|
+
num_shunting_slots=num_rotations_for_scenario[depot_station] // 10,
|
|
1223
|
+
num_cleaning_slots=num_rotations_for_scenario[depot_station] // 10,
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
# Delete all vehicles and events again. Only depot layout is kept
|
|
1227
|
+
|
|
1228
|
+
rotation_q = session.query(Rotation).filter(Rotation.scenario_id == scenario.id)
|
|
1229
|
+
rotation_q.update({"vehicle_id": None})
|
|
1230
|
+
session.query(Event).filter(Event.scenario_id == scenario.id).delete()
|
|
1231
|
+
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):
|
|
@@ -1070,7 +1070,8 @@ def depot_smallest_possible_size(
|
|
|
1070
1070
|
logger.debug(
|
|
1071
1071
|
f"Vehicle count for {vt.name} in {amount_of_line_areas} line areas configuration: {vehicle_count_q}. This is > than the all-direct configuration ({vehicle_counts_all_direct[vt]})."
|
|
1072
1072
|
)
|
|
1073
|
-
except
|
|
1073
|
+
except Exception as e:
|
|
1074
|
+
# This change is made after Unstable exception and delay exceptions are introduced
|
|
1074
1075
|
if (
|
|
1075
1076
|
"which suggests the fleet or the infrastructure might not be enough for the full electrification. Please add charging interfaces or increase charging power ."
|
|
1076
1077
|
in repr(e)
|
|
@@ -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
|
)
|
|
@@ -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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/layout_opt/opt_tools/fitness_c_urfd.py
RENAMED
|
File without changes
|
{eflips_depot-4.9.1 → eflips_depot-4.11.0}/eflips/depot/layout_opt/opt_tools/fitness_util.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|