eflips-depot 4.13.5__tar.gz → 4.14.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.
Files changed (42) hide show
  1. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/PKG-INFO +3 -2
  2. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/__init__.py +0 -43
  3. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/api/__init__.py +187 -18
  4. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/api/private/depot.py +363 -1
  5. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/api/private/results_to_database.py +48 -4
  6. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/pyproject.toml +1 -1
  7. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/LICENSE.md +0 -0
  8. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/README.md +0 -0
  9. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/api/defaults/default_settings.json +0 -0
  10. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/api/private/__init__.py +0 -0
  11. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/api/private/consumption.py +0 -0
  12. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/api/private/util.py +0 -0
  13. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/configuration.py +0 -0
  14. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/depot.py +0 -0
  15. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/evaluation.py +0 -0
  16. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/filters.py +0 -0
  17. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/input_epex_power_price.py +0 -0
  18. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/layout_opt/__init__.py +0 -0
  19. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/layout_opt/doc/__init__.py +0 -0
  20. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/layout_opt/doc/direct_details.pdf +0 -0
  21. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/layout_opt/evaluation.py +0 -0
  22. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/layout_opt/opt_tools/__init__.py +0 -0
  23. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/layout_opt/opt_tools/crossover.py +0 -0
  24. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/layout_opt/opt_tools/fitness_c_urfd.py +0 -0
  25. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/layout_opt/opt_tools/fitness_util.py +0 -0
  26. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/layout_opt/opt_tools/init.py +0 -0
  27. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/layout_opt/opt_tools/mutation.py +0 -0
  28. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/layout_opt/optimize_c_urfd.py +0 -0
  29. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/layout_opt/packing.py +0 -0
  30. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/layout_opt/settings.py +0 -0
  31. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/layout_opt/template_creation.py +0 -0
  32. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/layout_opt/util.py +0 -0
  33. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/plots.py +0 -0
  34. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/processes.py +0 -0
  35. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/rating.py +0 -0
  36. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/resources.py +0 -0
  37. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/settings_config.py +0 -0
  38. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/simple_vehicle.py +0 -0
  39. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/simulation.py +0 -0
  40. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/smart_charging.py +0 -0
  41. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/standalone.py +0 -0
  42. {eflips_depot-4.13.5 → eflips_depot-4.14.0}/eflips/depot/validation.py +0 -0
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: eflips-depot
3
- Version: 4.13.5
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
@@ -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
- )
@@ -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
- raise delay_exp
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
- raise unstable_exp
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
- errors = []
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
- if delay_exp.has_errors:
1092
- errors.append(delay_exp)
1093
- if unstable_exp.has_errors:
1094
- errors.append(unstable_exp)
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 UnstableSimulationException, DelayedTripException
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
- from eflips.depot import UnstableSimulationException, DelayedTripException
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
- if len(all_waiting_starts) == 0:
599
+ BUFFER_WAITING_CAPACITY = 1
600
+
601
+ if len(all_waiting_starts) < BUFFER_WAITING_CAPACITY:
558
602
  logger.info(
559
- "No waiting events found. The depot has enough capacity for waiting. Change the waiting area capacity to 10 as buffer."
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": 10}, synchronize_session="auto"
607
+ {"capacity": BUFFER_WAITING_CAPACITY}, synchronize_session="auto"
564
608
  )
565
609
 
566
610
  return
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "eflips-depot"
3
- version = "4.13.5"
3
+ version = "4.14.0"
4
4
  description = "Depot Simulation for eFLIPS"
5
5
  authors = ["Enrico Lauth <enrico.lauth@tu-berlin.de>",
6
6
  "Ludger Heide <ludger.heide@tu-berlin.de",
File without changes
File without changes