eflips-depot 4.13.5__py3-none-any.whl → 4.14.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/__init__.py +0 -43
- eflips/depot/api/__init__.py +187 -18
- eflips/depot/api/private/depot.py +363 -1
- eflips/depot/api/private/results_to_database.py +48 -4
- {eflips_depot-4.13.5.dist-info → eflips_depot-4.14.0.dist-info}/METADATA +3 -2
- {eflips_depot-4.13.5.dist-info → eflips_depot-4.14.0.dist-info}/RECORD +8 -8
- {eflips_depot-4.13.5.dist-info → eflips_depot-4.14.0.dist-info}/WHEEL +1 -1
- {eflips_depot-4.13.5.dist-info → eflips_depot-4.14.0.dist-info/licenses}/LICENSE.md +0 -0
eflips/depot/__init__.py
CHANGED
|
@@ -52,46 +52,3 @@ from eflips.depot.simulation import (
|
|
|
52
52
|
from eflips.depot.smart_charging import SmartCharging, ControlSmartCharging
|
|
53
53
|
from eflips.depot.standalone import VehicleGenerator, SimpleTrip, Timetable
|
|
54
54
|
from eflips.depot.validation import Validator
|
|
55
|
-
|
|
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
|
-
|
|
81
|
-
class UnstableSimulationException(Exception):
|
|
82
|
-
def __init__(self):
|
|
83
|
-
self._unstable_trips = []
|
|
84
|
-
|
|
85
|
-
def raise_later(self, simple_trip):
|
|
86
|
-
self._unstable_trips.append(simple_trip)
|
|
87
|
-
|
|
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
|
-
)
|
eflips/depot/api/__init__.py
CHANGED
|
@@ -32,7 +32,8 @@ from collections import OrderedDict
|
|
|
32
32
|
from datetime import timedelta, datetime
|
|
33
33
|
from enum import Enum
|
|
34
34
|
from math import ceil
|
|
35
|
-
from typing import Any, Dict, Optional, Union
|
|
35
|
+
from typing import Any, Dict, Optional, Union, List
|
|
36
|
+
|
|
36
37
|
|
|
37
38
|
import sqlalchemy.orm
|
|
38
39
|
from eflips.model import (
|
|
@@ -58,8 +59,6 @@ import eflips.depot
|
|
|
58
59
|
from eflips.depot import (
|
|
59
60
|
DepotEvaluation,
|
|
60
61
|
SimulationHost,
|
|
61
|
-
UnstableSimulationException,
|
|
62
|
-
DelayedTripException,
|
|
63
62
|
)
|
|
64
63
|
from eflips.depot.api.private.consumption import ConsumptionResult
|
|
65
64
|
from eflips.depot.api.private.consumption import (
|
|
@@ -74,6 +73,7 @@ from eflips.depot.api.private.depot import (
|
|
|
74
73
|
group_rotations_by_start_end_stop,
|
|
75
74
|
generate_depot,
|
|
76
75
|
depot_smallest_possible_size,
|
|
76
|
+
create_depots_from_wish,
|
|
77
77
|
)
|
|
78
78
|
from eflips.depot.api.private.results_to_database import (
|
|
79
79
|
get_finished_schedules_per_vehicle,
|
|
@@ -83,6 +83,8 @@ from eflips.depot.api.private.results_to_database import (
|
|
|
83
83
|
add_events_into_database,
|
|
84
84
|
update_vehicle_in_rotation,
|
|
85
85
|
update_waiting_events,
|
|
86
|
+
UnstableSimulationException,
|
|
87
|
+
DelayedTripException,
|
|
86
88
|
)
|
|
87
89
|
from eflips.depot.api.private.util import (
|
|
88
90
|
create_session,
|
|
@@ -93,6 +95,8 @@ from eflips.depot.api.private.util import (
|
|
|
93
95
|
check_depot_validity,
|
|
94
96
|
)
|
|
95
97
|
|
|
98
|
+
from eflips.depot.api.private.depot import AreaInformation, DepotConfigurationWish
|
|
99
|
+
|
|
96
100
|
|
|
97
101
|
class SmartChargingStrategy(Enum):
|
|
98
102
|
"""Enum class for different smart charging strategies."""
|
|
@@ -636,6 +640,8 @@ def simulate_scenario(
|
|
|
636
640
|
repetition_period=repetition_period,
|
|
637
641
|
)
|
|
638
642
|
ev = run_simulation(simulation_host)
|
|
643
|
+
|
|
644
|
+
errors = []
|
|
639
645
|
try:
|
|
640
646
|
add_evaluation_to_database(scenario, ev, session)
|
|
641
647
|
|
|
@@ -645,14 +651,19 @@ def simulate_scenario(
|
|
|
645
651
|
"There are delayed trips in the simulation. "
|
|
646
652
|
"Please check the input data and try again."
|
|
647
653
|
)
|
|
648
|
-
|
|
654
|
+
errors.append(delay_exp.exceptions[0])
|
|
649
655
|
except* UnstableSimulationException as unstable_exp:
|
|
650
656
|
if not ignore_unstable_simulation:
|
|
651
657
|
logger.error(
|
|
652
658
|
"The simulation became unstable. "
|
|
653
659
|
"Please check the input data and try again."
|
|
654
660
|
)
|
|
655
|
-
|
|
661
|
+
errors.append(unstable_exp.exceptions[0])
|
|
662
|
+
|
|
663
|
+
# There will be exactly one UnstableSimulationException or DelayedTripException in the list.
|
|
664
|
+
# Only the DelayedTripException will be raised if both exceptions are in the list.
|
|
665
|
+
for e in errors:
|
|
666
|
+
raise e
|
|
656
667
|
|
|
657
668
|
match smart_charging_strategy:
|
|
658
669
|
case SmartChargingStrategy.NONE:
|
|
@@ -1000,6 +1011,10 @@ def add_evaluation_to_database(
|
|
|
1000
1011
|
|
|
1001
1012
|
# Read simulation start time
|
|
1002
1013
|
|
|
1014
|
+
unstable_exp = UnstableSimulationException()
|
|
1015
|
+
delay_exp = DelayedTripException()
|
|
1016
|
+
errors = []
|
|
1017
|
+
|
|
1003
1018
|
for depot_id, depot_evaluation in depot_evaluations.items():
|
|
1004
1019
|
simulation_start_time = depot_evaluation.sim_start_datetime
|
|
1005
1020
|
|
|
@@ -1019,9 +1034,6 @@ def add_evaluation_to_database(
|
|
|
1019
1034
|
f"one waiting area."
|
|
1020
1035
|
)
|
|
1021
1036
|
|
|
1022
|
-
unstable_exp = UnstableSimulationException()
|
|
1023
|
-
delay_exp = DelayedTripException()
|
|
1024
|
-
|
|
1025
1037
|
for current_vehicle in depot_evaluation.vehicle_generator.items:
|
|
1026
1038
|
# Vehicle-layer operations
|
|
1027
1039
|
|
|
@@ -1086,17 +1098,15 @@ def add_evaluation_to_database(
|
|
|
1086
1098
|
update_vehicle_in_rotation(session, scenario, list_of_assigned_schedules)
|
|
1087
1099
|
update_waiting_events(session, scenario, waiting_area_id)
|
|
1088
1100
|
|
|
1089
|
-
|
|
1101
|
+
if delay_exp.has_errors:
|
|
1102
|
+
errors.append(delay_exp)
|
|
1103
|
+
if unstable_exp.has_errors:
|
|
1104
|
+
errors.append(unstable_exp)
|
|
1090
1105
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
if len(errors) > 0:
|
|
1097
|
-
raise ExceptionGroup(
|
|
1098
|
-
"Simulation is either unstable or including delayed blocks", errors
|
|
1099
|
-
)
|
|
1106
|
+
if len(errors) > 0:
|
|
1107
|
+
raise ExceptionGroup(
|
|
1108
|
+
"Simulation is either unstable or including delayed blocks", errors
|
|
1109
|
+
)
|
|
1100
1110
|
|
|
1101
1111
|
|
|
1102
1112
|
def generate_depot_optimal_size(
|
|
@@ -1334,3 +1344,162 @@ def schedule_duration_days(
|
|
|
1334
1344
|
duration_days = ceil(duration.total_seconds() / (24 * 60 * 60))
|
|
1335
1345
|
|
|
1336
1346
|
return timedelta(days=duration_days)
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
def auto_generate_depot_inplace(
|
|
1350
|
+
depot_wish: DepotConfigurationWish,
|
|
1351
|
+
session,
|
|
1352
|
+
scenario,
|
|
1353
|
+
) -> None:
|
|
1354
|
+
"""
|
|
1355
|
+
This method calculates an optimal depot layout for auto-generated depots and modifies the instance of
|
|
1356
|
+
:class:`eflips.depot.api.private.depot.DepotConfigurationWish` objects in place.
|
|
1357
|
+
|
|
1358
|
+
:param depot_wish: a :class:`eflips.depot.api.private.depot.DepotConfigurationWish` object. It represents a depot to be generated by eflips calulation.
|
|
1359
|
+
:param session: a :class:'sqlalchemy.orm.Session' object.
|
|
1360
|
+
:param scenario: a :class:`eflips.model.Scenario` object containing the input data for the simulation.
|
|
1361
|
+
:return: None. The depot layout will be written inplace to the depot_wish object.
|
|
1362
|
+
"""
|
|
1363
|
+
logger = logging.getLogger(__name__)
|
|
1364
|
+
|
|
1365
|
+
station = session.query(Station).filter(Station.id == depot_wish.station_id).one()
|
|
1366
|
+
|
|
1367
|
+
warnings.simplefilter("ignore", category=ConsistencyWarning)
|
|
1368
|
+
warnings.simplefilter("ignore", category=UserWarning)
|
|
1369
|
+
|
|
1370
|
+
inner_savepoint = session.begin_nested()
|
|
1371
|
+
try:
|
|
1372
|
+
# (Temporarily) Delete all rotations not starting or ending at the station
|
|
1373
|
+
logger.debug(f"Deleting all rotations not starting or ending at {station.name}")
|
|
1374
|
+
all_rot_for_scenario = (
|
|
1375
|
+
session.query(Rotation).filter(Rotation.scenario_id == scenario.id).all()
|
|
1376
|
+
)
|
|
1377
|
+
to_delete = []
|
|
1378
|
+
for rot in all_rot_for_scenario:
|
|
1379
|
+
first_stop = rot.trips[0].route.departure_station
|
|
1380
|
+
if first_stop != station:
|
|
1381
|
+
for trip in rot.trips:
|
|
1382
|
+
for stop_time in trip.stop_times:
|
|
1383
|
+
to_delete.append(stop_time)
|
|
1384
|
+
to_delete.append(trip)
|
|
1385
|
+
to_delete.append(rot)
|
|
1386
|
+
|
|
1387
|
+
# debugging
|
|
1388
|
+
for obj in to_delete:
|
|
1389
|
+
session.flush()
|
|
1390
|
+
session.delete(obj)
|
|
1391
|
+
session.flush()
|
|
1392
|
+
|
|
1393
|
+
# Consumption simulation for rotations of this depot
|
|
1394
|
+
|
|
1395
|
+
consumption_results = generate_consumption_result(scenario)
|
|
1396
|
+
simple_consumption_simulation(
|
|
1397
|
+
scenario,
|
|
1398
|
+
initialize_vehicles=True,
|
|
1399
|
+
consumption_result=consumption_results,
|
|
1400
|
+
)
|
|
1401
|
+
|
|
1402
|
+
logger.info(f"Generating depot layout for station {station.name}")
|
|
1403
|
+
vt_capacities_for_station = depot_smallest_possible_size(
|
|
1404
|
+
station,
|
|
1405
|
+
scenario,
|
|
1406
|
+
session,
|
|
1407
|
+
depot_wish.standard_block_length,
|
|
1408
|
+
depot_wish.default_power,
|
|
1409
|
+
)
|
|
1410
|
+
|
|
1411
|
+
area_list: List[AreaInformation] = []
|
|
1412
|
+
|
|
1413
|
+
for vt, capacity in vt_capacities_for_station.items():
|
|
1414
|
+
if capacity[AreaType.LINE] > 0:
|
|
1415
|
+
area_list.append(
|
|
1416
|
+
AreaInformation(
|
|
1417
|
+
area_type=AreaType.LINE,
|
|
1418
|
+
capacity=capacity[AreaType.LINE],
|
|
1419
|
+
block_length=depot_wish.standard_block_length,
|
|
1420
|
+
power=depot_wish.default_power,
|
|
1421
|
+
vehicle_type_id=vt.id,
|
|
1422
|
+
)
|
|
1423
|
+
)
|
|
1424
|
+
|
|
1425
|
+
if capacity[AreaType.DIRECT_ONESIDE] > 0:
|
|
1426
|
+
area_list.append(
|
|
1427
|
+
AreaInformation(
|
|
1428
|
+
area_type=AreaType.DIRECT_ONESIDE,
|
|
1429
|
+
capacity=capacity[AreaType.DIRECT_ONESIDE],
|
|
1430
|
+
block_length=None,
|
|
1431
|
+
power=depot_wish.default_power,
|
|
1432
|
+
vehicle_type_id=vt.id,
|
|
1433
|
+
)
|
|
1434
|
+
)
|
|
1435
|
+
|
|
1436
|
+
depot_wish.areas = area_list
|
|
1437
|
+
|
|
1438
|
+
finally:
|
|
1439
|
+
inner_savepoint.rollback()
|
|
1440
|
+
|
|
1441
|
+
session.expire_all()
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
def generate_optimal_depot_layout(
|
|
1445
|
+
depot_config_wishes: List[DepotConfigurationWish],
|
|
1446
|
+
scenario: Union[Scenario, int, Any],
|
|
1447
|
+
database_url: Optional[str] = None,
|
|
1448
|
+
delete_existing_depot: bool = False,
|
|
1449
|
+
) -> None:
|
|
1450
|
+
"""
|
|
1451
|
+
|
|
1452
|
+
:param depot_config_wishes: a list of :class:`eflips.depot.api.private.depot.DepotConfigurationWish` objects.
|
|
1453
|
+
:param scenario: Either a :class:`eflips.model.Scenario` object containing the input data for the simulation. Or
|
|
1454
|
+
an integer specifying the ID of a scenario in the database. Or any other object that has an attribute "id" containing
|
|
1455
|
+
an integer pointing to a unique scenario id.
|
|
1456
|
+
:param database_url: An optional database URL. Used if no database url is given by the environment variable.
|
|
1457
|
+
:param delete_existing_depot: If there is already a depot existing in this scenario, set True to delete this
|
|
1458
|
+
existing depot. Set to False and a ValueError will be raised if there is a depot
|
|
1459
|
+
:return: None. The depot layout will be added to the database.
|
|
1460
|
+
"""
|
|
1461
|
+
|
|
1462
|
+
with create_session(scenario, database_url) as (session, scenario):
|
|
1463
|
+
# Delete all depot events
|
|
1464
|
+
session.query(Event).filter(
|
|
1465
|
+
Event.scenario_id == scenario.id, Event.area_id.isnot(None)
|
|
1466
|
+
).delete()
|
|
1467
|
+
|
|
1468
|
+
if session.query(Depot).filter(Depot.scenario_id == scenario.id).count() != 0:
|
|
1469
|
+
if delete_existing_depot is False:
|
|
1470
|
+
raise ValueError(
|
|
1471
|
+
"Depot already exists. Set delete_existing_depot to True to delete it."
|
|
1472
|
+
)
|
|
1473
|
+
|
|
1474
|
+
delete_depots(scenario, session)
|
|
1475
|
+
|
|
1476
|
+
# Group all the rotations by their start and end stop (depot)
|
|
1477
|
+
|
|
1478
|
+
grouped_rotations = group_rotations_by_start_end_stop(scenario.id, session)
|
|
1479
|
+
assert len(grouped_rotations) == len(depot_config_wishes), (
|
|
1480
|
+
"The number of depot configuration wishes must be equal to the number of depots in the scenario."
|
|
1481
|
+
f"Found {len(depot_config_wishes)} wishes and {len(grouped_rotations)} depots."
|
|
1482
|
+
)
|
|
1483
|
+
|
|
1484
|
+
savepoint = session.begin_nested()
|
|
1485
|
+
|
|
1486
|
+
# Delete all vehicles and events, also disconnect the vehicles from the rotations
|
|
1487
|
+
rotation_q = session.query(Rotation).filter(Rotation.scenario_id == scenario.id)
|
|
1488
|
+
rotation_q.update({"vehicle_id": None})
|
|
1489
|
+
session.query(Event).filter(Event.scenario_id == scenario.id).delete()
|
|
1490
|
+
session.query(Vehicle).filter(Vehicle.scenario_id == scenario.id).delete()
|
|
1491
|
+
session.expire_all()
|
|
1492
|
+
for depot_wish in depot_config_wishes:
|
|
1493
|
+
if depot_wish.auto_generate is False:
|
|
1494
|
+
continue
|
|
1495
|
+
|
|
1496
|
+
auto_generate_depot_inplace(
|
|
1497
|
+
depot_wish,
|
|
1498
|
+
session,
|
|
1499
|
+
scenario,
|
|
1500
|
+
)
|
|
1501
|
+
|
|
1502
|
+
savepoint.rollback()
|
|
1503
|
+
create_depots_from_wish(
|
|
1504
|
+
depot_config_wishes, grouped_rotations, scenario, session
|
|
1505
|
+
)
|
|
@@ -4,6 +4,7 @@ import math
|
|
|
4
4
|
from datetime import timedelta
|
|
5
5
|
from enum import Enum, auto
|
|
6
6
|
from typing import Dict, List, Tuple, Optional
|
|
7
|
+
from dataclasses import dataclass
|
|
7
8
|
|
|
8
9
|
import numpy as np
|
|
9
10
|
import sqlalchemy.orm
|
|
@@ -27,13 +28,120 @@ from eflips.model import (
|
|
|
27
28
|
from sqlalchemy import or_
|
|
28
29
|
from sqlalchemy.orm import Session
|
|
29
30
|
|
|
30
|
-
from eflips.depot import
|
|
31
|
+
from eflips.depot.api.private.results_to_database import (
|
|
32
|
+
UnstableSimulationException,
|
|
33
|
+
DelayedTripException,
|
|
34
|
+
)
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
class MissingVehicleDimensionError(ValueError):
|
|
34
38
|
pass
|
|
35
39
|
|
|
36
40
|
|
|
41
|
+
@dataclass
|
|
42
|
+
class AreaInformation:
|
|
43
|
+
""" """
|
|
44
|
+
|
|
45
|
+
area_type: AreaType
|
|
46
|
+
"""
|
|
47
|
+
Type of the area, defined in AreaType enum.
|
|
48
|
+
"""
|
|
49
|
+
capacity: int
|
|
50
|
+
"""Capacity of the area."""
|
|
51
|
+
block_length: int | None
|
|
52
|
+
"""Block length of the area in meters. Only needed for AreaType.LINE."""
|
|
53
|
+
power: float
|
|
54
|
+
"""Charging power of the area in kW."""
|
|
55
|
+
vehicle_type_id: int
|
|
56
|
+
"""Vehicle type ID of the area."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, area_type, capacity, power, vehicle_type_id, block_length=None):
|
|
59
|
+
self.area_type = area_type
|
|
60
|
+
self.capacity = capacity
|
|
61
|
+
self.block_length = block_length
|
|
62
|
+
self.power = power
|
|
63
|
+
self.vehicle_type_id = vehicle_type_id
|
|
64
|
+
# do some simple validation here
|
|
65
|
+
if self.area_type == AreaType.LINE and (
|
|
66
|
+
self.block_length is None
|
|
67
|
+
or self.block_length <= 0
|
|
68
|
+
or self.capacity % self.block_length != 0
|
|
69
|
+
):
|
|
70
|
+
raise ValueError(
|
|
71
|
+
"Block length must be a positive integer for LINE areas, "
|
|
72
|
+
"and must be multiple of the standard block length."
|
|
73
|
+
)
|
|
74
|
+
if self.area_type != AreaType.LINE and self.block_length is not None:
|
|
75
|
+
raise ValueError("Block length must be None for non-LINE areas.")
|
|
76
|
+
|
|
77
|
+
if self.capacity <= 0:
|
|
78
|
+
raise ValueError("Capacity must be a positive integer.")
|
|
79
|
+
if self.power <= 0:
|
|
80
|
+
raise ValueError("Power must be a positive float.")
|
|
81
|
+
if self.vehicle_type_id <= 0:
|
|
82
|
+
raise ValueError("Vehicle type ID must be a positive integer.")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class DepotConfigurationWish:
|
|
87
|
+
|
|
88
|
+
""" """
|
|
89
|
+
|
|
90
|
+
station_id: int
|
|
91
|
+
"""
|
|
92
|
+
ID of the station where the depot should be located.
|
|
93
|
+
"""
|
|
94
|
+
auto_generate: bool
|
|
95
|
+
"""
|
|
96
|
+
True if the depot should be auto-generated, False if the depot should be created from user input.
|
|
97
|
+
"""
|
|
98
|
+
default_power: float | None
|
|
99
|
+
standard_block_length: int | None
|
|
100
|
+
cleaning_slots: int | None
|
|
101
|
+
cleaning_duration: timedelta | None
|
|
102
|
+
shunting_slots: int | None
|
|
103
|
+
shunting_duration: timedelta | None
|
|
104
|
+
areas: list[AreaInformation] | None #
|
|
105
|
+
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
station_id,
|
|
109
|
+
auto_generate: bool = False,
|
|
110
|
+
default_power: float | None = None,
|
|
111
|
+
standard_block_length: int | None = None,
|
|
112
|
+
cleaning_slots: int | None = None,
|
|
113
|
+
cleaning_duration: timedelta | None = None,
|
|
114
|
+
shunting_slots: int | None = None,
|
|
115
|
+
shunting_duration: timedelta | None = None,
|
|
116
|
+
areas: list[AreaInformation] | None = None,
|
|
117
|
+
):
|
|
118
|
+
self.station_id = station_id
|
|
119
|
+
self.auto_generate = auto_generate
|
|
120
|
+
self.default_power = default_power
|
|
121
|
+
self.standard_block_length = standard_block_length
|
|
122
|
+
self.cleaning_slots = cleaning_slots
|
|
123
|
+
self.cleaning_duration = cleaning_duration
|
|
124
|
+
self.shunting_slots = shunting_slots
|
|
125
|
+
self.shunting_duration = shunting_duration
|
|
126
|
+
self.areas = areas
|
|
127
|
+
|
|
128
|
+
if self.auto_generate is True:
|
|
129
|
+
if self.default_power is None or self.standard_block_length is None:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
"If auto_generate is True, default_power, standard_block_length, cleaning_slots, cleaning_duration and shunting_slots must be provided."
|
|
132
|
+
)
|
|
133
|
+
if (
|
|
134
|
+
self.cleaning_slots is not None
|
|
135
|
+
or self.cleaning_duration is not None
|
|
136
|
+
or self.shunting_slots is not None
|
|
137
|
+
or self.shunting_duration is not None
|
|
138
|
+
):
|
|
139
|
+
raise ValueError(
|
|
140
|
+
"If auto_generate is True, default_power, standard_block_length, cleaning_slots, cleaning_duration and shunting_slots cannot be provided."
|
|
141
|
+
)
|
|
142
|
+
# do some simple validation here
|
|
143
|
+
|
|
144
|
+
|
|
37
145
|
def delete_depots(scenario: Scenario, session: Session) -> None:
|
|
38
146
|
"""This function deletes all depot-related data from the database for a given scenario.
|
|
39
147
|
|
|
@@ -1117,3 +1225,257 @@ def depot_smallest_possible_size(
|
|
|
1117
1225
|
return ret_val
|
|
1118
1226
|
finally:
|
|
1119
1227
|
outer_savepoint.rollback()
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
def estimate_service_capacity(
|
|
1231
|
+
all_rotations_this_depot: List[Rotation], service_duration: timedelta
|
|
1232
|
+
) -> int:
|
|
1233
|
+
"""
|
|
1234
|
+
Estimate the number of service slots needed based on the rotations and a given service duration.
|
|
1235
|
+
It is estimated as the maximum arrivals in the time window of the service duration.
|
|
1236
|
+
:param all_rotations_this_depot: A list of all rotations starting and ending at the depot's station.
|
|
1237
|
+
:param service_duration: A timedelta representing the duration of the service.
|
|
1238
|
+
:return: Estimated number of service slots needed without waiting.
|
|
1239
|
+
"""
|
|
1240
|
+
if not all_rotations_this_depot:
|
|
1241
|
+
return 0
|
|
1242
|
+
|
|
1243
|
+
# Sort by the first trip’s departure time
|
|
1244
|
+
all_rotations_this_depot.sort(key=lambda r: r.trips[0].arrival_time)
|
|
1245
|
+
|
|
1246
|
+
max_arrivals = 0
|
|
1247
|
+
left = 0
|
|
1248
|
+
|
|
1249
|
+
# Sliding window: move right pointer
|
|
1250
|
+
for right, rotation in enumerate(all_rotations_this_depot):
|
|
1251
|
+
start_time = rotation.trips[0].departure_time
|
|
1252
|
+
|
|
1253
|
+
# Shrink window from the left until within service_duration
|
|
1254
|
+
while (
|
|
1255
|
+
start_time - all_rotations_this_depot[left].trips[0].departure_time
|
|
1256
|
+
> service_duration
|
|
1257
|
+
):
|
|
1258
|
+
left += 1
|
|
1259
|
+
|
|
1260
|
+
# Window size = number of rotations within the service duration
|
|
1261
|
+
max_arrivals = max(max_arrivals, right - left + 1)
|
|
1262
|
+
|
|
1263
|
+
return max_arrivals
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
def create_depots_from_wish(
|
|
1267
|
+
depot_config_wishes: List[DepotConfigurationWish],
|
|
1268
|
+
grouped_rotations: Dict[Tuple[Station, Station], Dict[VehicleType, List[Rotation]]],
|
|
1269
|
+
scenario: Scenario,
|
|
1270
|
+
session: Session,
|
|
1271
|
+
) -> None:
|
|
1272
|
+
"""
|
|
1273
|
+
This function only creates depots and their areas based on the provided configuration wishes. the result will be
|
|
1274
|
+
written into the database.
|
|
1275
|
+
|
|
1276
|
+
:param depot_config_wishes: A list of depot configuration wishes.
|
|
1277
|
+
:param grouped_rotations: A dictionary of grouped rotations by (start_station, end_station)
|
|
1278
|
+
:param scenario: The scenario to be simulated.
|
|
1279
|
+
:param session: An open SQLAlchemy session.
|
|
1280
|
+
:return: None. The depots are added to the database.
|
|
1281
|
+
"""
|
|
1282
|
+
for depot_wish in depot_config_wishes:
|
|
1283
|
+
station = (
|
|
1284
|
+
session.query(Station).filter(Station.id == depot_wish.station_id).one()
|
|
1285
|
+
)
|
|
1286
|
+
depot = Depot(
|
|
1287
|
+
scenario=scenario,
|
|
1288
|
+
name=f"Depot at {station.name}",
|
|
1289
|
+
name_short=station.name_short,
|
|
1290
|
+
station_id=station.id,
|
|
1291
|
+
)
|
|
1292
|
+
session.add(depot)
|
|
1293
|
+
|
|
1294
|
+
plan = Plan(scenario=scenario, name=f"Default Plan")
|
|
1295
|
+
session.add(plan)
|
|
1296
|
+
|
|
1297
|
+
depot.default_plan = plan
|
|
1298
|
+
|
|
1299
|
+
all_rotations_with_type = grouped_rotations[station, station]
|
|
1300
|
+
all_rotations_this_depot = [
|
|
1301
|
+
r for vt, rotations in all_rotations_with_type.items() for r in rotations
|
|
1302
|
+
]
|
|
1303
|
+
|
|
1304
|
+
# Create processes
|
|
1305
|
+
|
|
1306
|
+
plan_process_assocs: List[AssocPlanProcess] = []
|
|
1307
|
+
|
|
1308
|
+
if depot_wish.shunting_duration is None:
|
|
1309
|
+
shunting_duration = timedelta(minutes=5)
|
|
1310
|
+
else:
|
|
1311
|
+
shunting_duration = depot_wish.shunting_duration
|
|
1312
|
+
if depot_wish.cleaning_duration is None:
|
|
1313
|
+
cleaning_duration = timedelta(minutes=30)
|
|
1314
|
+
else:
|
|
1315
|
+
cleaning_duration = depot_wish.cleaning_duration
|
|
1316
|
+
|
|
1317
|
+
if depot_wish.shunting_slots is None:
|
|
1318
|
+
shunting_slots = estimate_service_capacity(
|
|
1319
|
+
all_rotations_this_depot, shunting_duration
|
|
1320
|
+
)
|
|
1321
|
+
else:
|
|
1322
|
+
shunting_slots = depot_wish.shunting_slots
|
|
1323
|
+
shunting_1 = Process(
|
|
1324
|
+
name="Shunting 1",
|
|
1325
|
+
scenario=scenario,
|
|
1326
|
+
dispatchable=False,
|
|
1327
|
+
duration=shunting_duration,
|
|
1328
|
+
)
|
|
1329
|
+
session.add(shunting_1)
|
|
1330
|
+
shunting_area_1 = Area(
|
|
1331
|
+
scenario=scenario,
|
|
1332
|
+
name=f"Shunting Area 1",
|
|
1333
|
+
depot=depot,
|
|
1334
|
+
area_type=AreaType.DIRECT_ONESIDE,
|
|
1335
|
+
vehicle_type=None, # Meaning any vehicle type can be shunted here
|
|
1336
|
+
capacity=shunting_slots,
|
|
1337
|
+
)
|
|
1338
|
+
session.add(shunting_area_1)
|
|
1339
|
+
shunting_area_1.processes.append(shunting_1)
|
|
1340
|
+
plan_process_assocs.append(
|
|
1341
|
+
AssocPlanProcess(
|
|
1342
|
+
scenario=scenario,
|
|
1343
|
+
process=shunting_1,
|
|
1344
|
+
plan=plan,
|
|
1345
|
+
ordinal=len(plan_process_assocs),
|
|
1346
|
+
)
|
|
1347
|
+
)
|
|
1348
|
+
|
|
1349
|
+
if depot_wish.cleaning_slots is None:
|
|
1350
|
+
cleaning_slots = estimate_service_capacity(
|
|
1351
|
+
all_rotations_this_depot, cleaning_duration
|
|
1352
|
+
)
|
|
1353
|
+
else:
|
|
1354
|
+
cleaning_slots = depot_wish.cleaning_slots
|
|
1355
|
+
clean = Process(
|
|
1356
|
+
name="Arrival Cleaning",
|
|
1357
|
+
scenario=scenario,
|
|
1358
|
+
dispatchable=False,
|
|
1359
|
+
duration=cleaning_duration,
|
|
1360
|
+
)
|
|
1361
|
+
session.add(clean)
|
|
1362
|
+
cleaning_area = Area(
|
|
1363
|
+
scenario=scenario,
|
|
1364
|
+
name=f"Cleaning Area",
|
|
1365
|
+
depot=depot,
|
|
1366
|
+
area_type=AreaType.DIRECT_ONESIDE,
|
|
1367
|
+
vehicle_type=None, # Meaning any vehicle type can be cleaned here
|
|
1368
|
+
capacity=cleaning_slots,
|
|
1369
|
+
)
|
|
1370
|
+
session.add(cleaning_area)
|
|
1371
|
+
cleaning_area.processes.append(clean)
|
|
1372
|
+
plan_process_assocs.append(
|
|
1373
|
+
AssocPlanProcess(
|
|
1374
|
+
scenario=scenario,
|
|
1375
|
+
process=clean,
|
|
1376
|
+
plan=plan,
|
|
1377
|
+
ordinal=len(plan_process_assocs),
|
|
1378
|
+
)
|
|
1379
|
+
)
|
|
1380
|
+
|
|
1381
|
+
shunting_2 = Process(
|
|
1382
|
+
name="Shunting 2",
|
|
1383
|
+
scenario=scenario,
|
|
1384
|
+
dispatchable=False,
|
|
1385
|
+
duration=shunting_duration,
|
|
1386
|
+
)
|
|
1387
|
+
session.add(shunting_2)
|
|
1388
|
+
shunting_area_2 = Area(
|
|
1389
|
+
scenario=scenario,
|
|
1390
|
+
name=f"Shunting Area 2",
|
|
1391
|
+
depot=depot,
|
|
1392
|
+
area_type=AreaType.DIRECT_ONESIDE,
|
|
1393
|
+
vehicle_type=None, # Meaning any vehicle type can be shunted here
|
|
1394
|
+
capacity=shunting_slots,
|
|
1395
|
+
)
|
|
1396
|
+
session.add(shunting_area_2)
|
|
1397
|
+
shunting_area_2.processes.append(shunting_2)
|
|
1398
|
+
plan_process_assocs.append(
|
|
1399
|
+
AssocPlanProcess(
|
|
1400
|
+
scenario=scenario,
|
|
1401
|
+
process=shunting_2,
|
|
1402
|
+
plan=plan,
|
|
1403
|
+
ordinal=len(plan_process_assocs),
|
|
1404
|
+
)
|
|
1405
|
+
)
|
|
1406
|
+
|
|
1407
|
+
# TODO: we only do constant charging power per depot for now. Might be possible in the future to have different
|
|
1408
|
+
# powers per area
|
|
1409
|
+
|
|
1410
|
+
if depot_wish.auto_generate:
|
|
1411
|
+
charging_power = depot_wish.default_power
|
|
1412
|
+
else:
|
|
1413
|
+
charging_power = depot_wish.areas[0].power
|
|
1414
|
+
|
|
1415
|
+
charging = Process(
|
|
1416
|
+
name="Charging",
|
|
1417
|
+
scenario=scenario,
|
|
1418
|
+
dispatchable=True,
|
|
1419
|
+
electric_power=charging_power,
|
|
1420
|
+
)
|
|
1421
|
+
session.add(charging)
|
|
1422
|
+
plan_process_assocs.append(
|
|
1423
|
+
AssocPlanProcess(
|
|
1424
|
+
scenario=scenario,
|
|
1425
|
+
process=charging,
|
|
1426
|
+
plan=plan,
|
|
1427
|
+
ordinal=len(plan_process_assocs),
|
|
1428
|
+
)
|
|
1429
|
+
)
|
|
1430
|
+
|
|
1431
|
+
standby_departure = Process(
|
|
1432
|
+
name="Standby Pre-departure",
|
|
1433
|
+
scenario=scenario,
|
|
1434
|
+
dispatchable=True,
|
|
1435
|
+
)
|
|
1436
|
+
session.add(standby_departure)
|
|
1437
|
+
plan_process_assocs.append(
|
|
1438
|
+
AssocPlanProcess(
|
|
1439
|
+
scenario=scenario,
|
|
1440
|
+
process=standby_departure,
|
|
1441
|
+
plan=plan,
|
|
1442
|
+
ordinal=len(plan_process_assocs),
|
|
1443
|
+
)
|
|
1444
|
+
)
|
|
1445
|
+
session.add_all(plan_process_assocs) # It's complete, so add all at once
|
|
1446
|
+
|
|
1447
|
+
# Create shared waiting area
|
|
1448
|
+
rotation_count = len(
|
|
1449
|
+
session.query(Rotation).filter(Rotation.scenario_id == scenario.id).all()
|
|
1450
|
+
)
|
|
1451
|
+
|
|
1452
|
+
waiting_area = Area(
|
|
1453
|
+
scenario=scenario,
|
|
1454
|
+
name=f"Waiting Area for every type of vehicle",
|
|
1455
|
+
depot=depot,
|
|
1456
|
+
area_type=AreaType.DIRECT_ONESIDE,
|
|
1457
|
+
capacity=rotation_count * 4,
|
|
1458
|
+
# Initialize with 4 times of rotation count because all rotations are copied three times. Assuming each rotation needs a vehicle.
|
|
1459
|
+
)
|
|
1460
|
+
session.add(waiting_area)
|
|
1461
|
+
|
|
1462
|
+
for area_info in depot_wish.areas:
|
|
1463
|
+
area = Area(
|
|
1464
|
+
scenario=scenario,
|
|
1465
|
+
name=f"{area_info.area_type.name} Area for {session.query(VehicleType).filter(VehicleType.id == area_info.vehicle_type_id).one().name_short}",
|
|
1466
|
+
depot=depot,
|
|
1467
|
+
area_type=area_info.area_type,
|
|
1468
|
+
vehicle_type_id=area_info.vehicle_type_id,
|
|
1469
|
+
capacity=area_info.capacity,
|
|
1470
|
+
row_count=(
|
|
1471
|
+
area_info.capacity // area_info.block_length
|
|
1472
|
+
if area_info.area_type == AreaType.LINE
|
|
1473
|
+
and area_info.block_length is not None
|
|
1474
|
+
else None
|
|
1475
|
+
),
|
|
1476
|
+
)
|
|
1477
|
+
area.processes.append(charging)
|
|
1478
|
+
area.processes.append(standby_departure)
|
|
1479
|
+
session.add(area)
|
|
1480
|
+
|
|
1481
|
+
session.flush()
|
|
@@ -10,7 +10,49 @@ from eflips.model import Event, EventType, Rotation, Vehicle, Area, AreaType
|
|
|
10
10
|
from sqlalchemy import select
|
|
11
11
|
|
|
12
12
|
from eflips.depot import SimpleVehicle, ProcessStatus
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DelayedTripException(Exception):
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self._delayed_trips = []
|
|
18
|
+
|
|
19
|
+
def raise_later(self, simple_trip):
|
|
20
|
+
self._delayed_trips.append(simple_trip)
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def has_errors(self):
|
|
24
|
+
return len(self._delayed_trips) > 0
|
|
25
|
+
|
|
26
|
+
def __str__(self):
|
|
27
|
+
trip_names = ", ".join(
|
|
28
|
+
f"{trip.ID} originally departure at {trip.std}"
|
|
29
|
+
for trip in self._delayed_trips
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
f"The following blocks/rotations are delayed. "
|
|
34
|
+
f"Ignoring this error will write related depot events into database. However, this may lead to errors due "
|
|
35
|
+
f"to conflicts with driving events: {trip_names}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class UnstableSimulationException(Exception):
|
|
40
|
+
def __init__(self):
|
|
41
|
+
self._unstable_trips = []
|
|
42
|
+
|
|
43
|
+
def raise_later(self, simple_trip):
|
|
44
|
+
self._unstable_trips.append(simple_trip)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def has_errors(self):
|
|
48
|
+
return len(self._unstable_trips) > 0
|
|
49
|
+
|
|
50
|
+
def __str__(self):
|
|
51
|
+
trip_names = ", ".join(str(trip.ID) for trip in self._unstable_trips)
|
|
52
|
+
return (
|
|
53
|
+
f"The following blocks/rotations require a new vehicle. This suggests an unstable "
|
|
54
|
+
f" simulation result, where a repeated schedule might require more vehicles: {trip_names}"
|
|
55
|
+
)
|
|
14
56
|
|
|
15
57
|
|
|
16
58
|
def get_finished_schedules_per_vehicle(
|
|
@@ -554,13 +596,15 @@ def update_waiting_events(session, scenario, waiting_area_id) -> None:
|
|
|
554
596
|
all_waiting_ends
|
|
555
597
|
), f"Number of waiting events starts {len(all_waiting_starts)} is not equal to the number of waiting event ends"
|
|
556
598
|
|
|
557
|
-
|
|
599
|
+
BUFFER_WAITING_CAPACITY = 1
|
|
600
|
+
|
|
601
|
+
if len(all_waiting_starts) < BUFFER_WAITING_CAPACITY:
|
|
558
602
|
logger.info(
|
|
559
|
-
"
|
|
603
|
+
f"{all_waiting_starts} waiting events found. The depot has enough capacity for waiting. Change the waiting area capacity to {BUFFER_WAITING_CAPACITY} as buffer."
|
|
560
604
|
)
|
|
561
605
|
|
|
562
606
|
session.query(Area).filter(Area.id == waiting_area_id).update(
|
|
563
|
-
{"capacity":
|
|
607
|
+
{"capacity": BUFFER_WAITING_CAPACITY}, synchronize_session="auto"
|
|
564
608
|
)
|
|
565
609
|
|
|
566
610
|
return
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: eflips-depot
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.14.0
|
|
4
4
|
Summary: Depot Simulation for eFLIPS
|
|
5
5
|
License: AGPL-3.0-or-later
|
|
6
|
+
License-File: LICENSE.md
|
|
6
7
|
Author: Enrico Lauth
|
|
7
8
|
Author-email: enrico.lauth@tu-berlin.de
|
|
8
9
|
Requires-Python: >=3.11,<3.14
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
eflips/depot/__init__.py,sha256=
|
|
2
|
-
eflips/depot/api/__init__.py,sha256=
|
|
1
|
+
eflips/depot/__init__.py,sha256=FCVnYU7aSfR4BNCo722I1V3uU5336qr1tTt-zT6X4UI,1555
|
|
2
|
+
eflips/depot/api/__init__.py,sha256=TDjYjf2h_oBwm3Wmcw1J1RQrV2xct7ETOMsgVZ6c30A,65406
|
|
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=wlC3uKD0H9XjES2Jfow4PTzQ0Jd7fxryIj57H1mY50g,28743
|
|
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=c4XsRN06PScy4Z4yocqrdvMntmgOZdbIxNT9uuN2lxA,56857
|
|
7
|
+
eflips/depot/api/private/results_to_database.py,sha256=nvWb4jEM5JwPAhvAdiQ-qzxUSawfRNsYfv3twNUFcGs,27348
|
|
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.
|
|
40
|
-
eflips_depot-4.
|
|
41
|
-
eflips_depot-4.
|
|
42
|
-
eflips_depot-4.
|
|
39
|
+
eflips_depot-4.14.0.dist-info/METADATA,sha256=E_QEqP-LBhuJ5pLcP6CGBdFnoDcl661xUYeQ-BCQONw,5961
|
|
40
|
+
eflips_depot-4.14.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
41
|
+
eflips_depot-4.14.0.dist-info/licenses/LICENSE.md,sha256=KB4XTk1fPHjtZCYDyPyreu6h1LVJVZXYg-5vePcWZAc,34143
|
|
42
|
+
eflips_depot-4.14.0.dist-info/RECORD,,
|
|
File without changes
|