eflips-depot 4.13.0__py3-none-any.whl → 4.13.2__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.
Potentially problematic release.
This version of eflips-depot might be problematic. Click here for more details.
- eflips/depot/__init__.py +43 -3
- eflips/depot/api/__init__.py +176 -85
- eflips/depot/api/private/depot.py +14 -3
- eflips/depot/api/private/results_to_database.py +54 -34
- {eflips_depot-4.13.0.dist-info → eflips_depot-4.13.2.dist-info}/METADATA +1 -1
- {eflips_depot-4.13.0.dist-info → eflips_depot-4.13.2.dist-info}/RECORD +8 -8
- {eflips_depot-4.13.0.dist-info → eflips_depot-4.13.2.dist-info}/LICENSE.md +0 -0
- {eflips_depot-4.13.0.dist-info → eflips_depot-4.13.2.dist-info}/WHEEL +0 -0
eflips/depot/__init__.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
eflips/depot/api/__init__.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
|
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
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
)
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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=
|
|
1252
|
-
num_cleaning_slots=
|
|
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=
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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,10 +1,10 @@
|
|
|
1
|
-
eflips/depot/__init__.py,sha256=
|
|
2
|
-
eflips/depot/api/__init__.py,sha256=
|
|
1
|
+
eflips/depot/__init__.py,sha256=p-oqm1LmTuIk5PRiWDyychBhwAsr-9Rw7dKmRCZ58wc,2968
|
|
2
|
+
eflips/depot/api/__init__.py,sha256=a3hA8iiogTenthVkEd-u4H2hwCz3725Qm_LujgGSHPw,58898
|
|
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
5
|
eflips/depot/api/private/consumption.py,sha256=nImegyhKrZlEwKTNdmAmDxjTQCvwfPgxA588e0M_F9o,27282
|
|
6
|
-
eflips/depot/api/private/depot.py,sha256=
|
|
7
|
-
eflips/depot/api/private/results_to_database.py,sha256=
|
|
6
|
+
eflips/depot/api/private/depot.py,sha256=uESFVBhHybDN-LzvceKHlFSfsVsBfh5gaOuTxspX6rY,44055
|
|
7
|
+
eflips/depot/api/private/results_to_database.py,sha256=FBlZRKqAIG80tFyuf5vCGHRycWKYkm53jFxJEk9t1NA,25984
|
|
8
8
|
eflips/depot/api/private/util.py,sha256=DasTkuGUhlBpY_BtTFWoxSNZU_CRyM3RqEDgO07Eks8,17990
|
|
9
9
|
eflips/depot/configuration.py,sha256=Op3hlir-dEN7yHr0kTqbYANoCBKFWK6uKOv3NJl8w_w,35678
|
|
10
10
|
eflips/depot/depot.py,sha256=pREutXtJlDxjgfwRobAy7UqTHh-tldbVWHN8DIyxs8s,106986
|
|
@@ -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.13.
|
|
40
|
-
eflips_depot-4.13.
|
|
41
|
-
eflips_depot-4.13.
|
|
42
|
-
eflips_depot-4.13.
|
|
39
|
+
eflips_depot-4.13.2.dist-info/LICENSE.md,sha256=KB4XTk1fPHjtZCYDyPyreu6h1LVJVZXYg-5vePcWZAc,34143
|
|
40
|
+
eflips_depot-4.13.2.dist-info/METADATA,sha256=QBChXaVYyrSBpjGL2uA2pXcci7RnGAUKNylOWjB3w5c,5987
|
|
41
|
+
eflips_depot-4.13.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
42
|
+
eflips_depot-4.13.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|