eflips-depot 4.4.3__tar.gz → 4.5.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.4.3 → eflips_depot-4.5.0}/PKG-INFO +3 -3
  2. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/__init__.py +4 -0
  3. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/api/__init__.py +259 -74
  4. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/pyproject.toml +1 -1
  5. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/LICENSE.md +0 -0
  6. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/README.md +0 -0
  7. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/api/defaults/default_settings.json +0 -0
  8. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/api/private/__init__.py +0 -0
  9. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/api/private/depot.py +0 -0
  10. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/api/private/results_to_database.py +0 -0
  11. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/api/private/smart_charging.py +0 -0
  12. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/api/private/util.py +0 -0
  13. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/configuration.py +0 -0
  14. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/depot.py +0 -0
  15. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/evaluation.py +0 -0
  16. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/filters.py +0 -0
  17. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/input_epex_power_price.py +0 -0
  18. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/layout_opt/__init__.py +0 -0
  19. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/layout_opt/doc/__init__.py +0 -0
  20. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/layout_opt/doc/direct_details.pdf +0 -0
  21. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/layout_opt/evaluation.py +0 -0
  22. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/layout_opt/opt_tools/__init__.py +0 -0
  23. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/layout_opt/opt_tools/crossover.py +0 -0
  24. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/layout_opt/opt_tools/fitness_c_urfd.py +0 -0
  25. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/layout_opt/opt_tools/fitness_util.py +0 -0
  26. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/layout_opt/opt_tools/init.py +0 -0
  27. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/layout_opt/opt_tools/mutation.py +0 -0
  28. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/layout_opt/optimize_c_urfd.py +0 -0
  29. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/layout_opt/packing.py +0 -0
  30. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/layout_opt/settings.py +0 -0
  31. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/layout_opt/template_creation.py +0 -0
  32. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/layout_opt/util.py +0 -0
  33. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/plots.py +0 -0
  34. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/processes.py +0 -0
  35. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/rating.py +0 -0
  36. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/resources.py +0 -0
  37. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/settings_config.py +0 -0
  38. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/simple_vehicle.py +0 -0
  39. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/simulation.py +0 -0
  40. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/smart_charging.py +0 -0
  41. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/standalone.py +0 -0
  42. {eflips_depot-4.4.3 → eflips_depot-4.5.0}/eflips/depot/validation.py +0 -0
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: eflips-depot
3
- Version: 4.4.3
3
+ Version: 4.5.0
4
4
  Summary: Depot Simulation for eFLIPS
5
- Home-page: https://github.com/mpm-tu-berlin/eflips-depot
6
5
  License: AGPL-3.0-or-later
7
6
  Author: Enrico Lauth
8
7
  Author-email: enrico.lauth@tu-berlin.de
@@ -21,6 +20,7 @@ Requires-Dist: simpy (>=4.0.1,<5.0.0)
21
20
  Requires-Dist: tqdm (>=4.67.0,<5.0.0)
22
21
  Requires-Dist: xlrd (<=1.2.0)
23
22
  Requires-Dist: xlsxwriter (>=3.1.9,<4.0.0)
23
+ Project-URL: Homepage, https://github.com/mpm-tu-berlin/eflips-depot
24
24
  Project-URL: Repository, https://github.com/mpm-tu-berlin/eflips-depot
25
25
  Description-Content-Type: text/markdown
26
26
 
@@ -53,3 +53,7 @@ from eflips.depot.simulation import (
53
53
  from eflips.depot.smart_charging import SmartCharging, ControlSmartCharging
54
54
  from eflips.depot.standalone import VehicleGenerator, SimpleTrip, Timetable
55
55
  from eflips.depot.validation import Validator
56
+
57
+
58
+ class UnstableSimulationException(Exception):
59
+ pass
@@ -25,15 +25,15 @@ The following steps are recommended for using the API:
25
25
  b. Run the :func:`simple_consumption_simulation` function again, this time with ``initialize_vehicles=False``.
26
26
  """
27
27
  import copy
28
- import datetime
29
28
  import logging
30
29
  import os
31
30
  import warnings
32
31
  from collections import OrderedDict
33
- from datetime import timedelta
32
+ from dataclasses import dataclass
33
+ from datetime import timedelta, datetime
34
34
  from enum import Enum
35
35
  from math import ceil
36
- from typing import Any, Dict, Optional, Union
36
+ from typing import Any, Dict, Optional, Union, List
37
37
 
38
38
  import sqlalchemy.orm
39
39
  from eflips.model import (
@@ -107,47 +107,131 @@ class SmartChargingStrategy(Enum):
107
107
  """
108
108
 
109
109
 
110
+ @dataclass
111
+ class ConsumptionResult:
112
+ """
113
+ A dataclass that stores the results of a charging simulation for a single trip.
114
+
115
+ This class holds both the total change in battery State of Charge (SoC) over the trip
116
+ as well as an optional timeseries of timestamps and incremental SoC changes. When
117
+ an entry exists for a given trip in ``consumption_result``, the simulation will use
118
+ these precomputed values instead of recalculating the SoC changes from the vehicle
119
+ distance and consumption.
120
+
121
+ :param delta_soc_total:
122
+ The total change in the vehicle's State of Charge over the trip, typically
123
+ negative if the vehicle is consuming energy (e.g., -0.15 means the SoC
124
+ dropped by 15%).
125
+
126
+ :param timestamps:
127
+ A list of timestamps (e.g., arrival times at stops) that mark the times
128
+ associated with the SoC changes. The number of timestamps must match the
129
+ number of entries in ``delta_soc``.
130
+
131
+ :param delta_soc:
132
+ A list of cumulative SoC changes corresponding to the ``timestamps``.
133
+ For example, if ``delta_soc[i] = -0.02``, it means the SoC decreased by 2%
134
+ between from the start of the trip to ``timestamps[i]``. This list should typically
135
+ be a monotonic decreasing sequence.
136
+ """
137
+
138
+ delta_soc_total: float
139
+ timestamps: List[datetime] | None
140
+ delta_soc: List[float] | None
141
+
142
+
110
143
  def simple_consumption_simulation(
111
144
  scenario: Union[Scenario, int, Any],
112
145
  initialize_vehicles: bool,
113
146
  database_url: Optional[str] = None,
114
147
  calculate_timeseries: bool = False,
115
148
  terminus_deadtime: timedelta = timedelta(minutes=1),
149
+ consumption_result: Dict[int, ConsumptionResult] | None = None,
116
150
  ) -> None:
117
151
  """
118
- A simple consumption simulation and vehicle initialization.
119
-
120
- Energy consumotion is calculated by multiplying the vehicle's total distance by a constant
121
- ``VehicleType.consumption``.
122
-
123
- If run with ``initialize_vehicles=True``, the method will also initialize the vehicles in the database with the
124
- correct vehicle type and assign them to rotations. If this is false, it will assume that there are already vehicle
125
- entries and ``Rotation.vehicle_id`` is already set.
126
-
127
- :param scenario: Either a :class:`eflips.model.Scenario` object containing the input data for the simulation. Or
128
- an integer specifying the ID of a scenario in the database. Or any other object that has an attribute
129
- ``id`` that is an integer. If no :class:`eflips.model.Scenario` object is passed, the ``database_url``
130
- parameter must be set to a valid database URL ot the environment variable ``DATABASE_URL`` must be set to a
131
- valid database URL.
132
-
133
- :param initialize_vehicles: A boolean flag indicating whether the vehicles should be initialized in the database.
134
- When running this function for the first time, this should be set to True. When running this function again
135
- after the vehicles have been initialized, this should be set to False.
136
-
137
- :param database_url: An optional database URL. If no database URL is passed and the `scenario` parameter is not a
138
- :class:`eflips.model.Scenario` object, the environment variable `DATABASE_URL` must be set to a
139
- valid database URL.
140
-
141
- :param calculate_timeseries: A boolean flag indicating whether the timeseries should be calculated. If this is set
142
- to True, the SoC at each stop is calculated and added to the "timeseries" column of the Event table. If this
143
- is set to False, the "timeseries" column of the Event table will be set to ``None``. Setting this to false
144
- may significantly speed up the simulation.
145
-
146
- :param terminus_deadtime: The total deadtime taken to both attach and detach the charging cable at the terminus.
147
- If the total deadtime is greater than the time between the arrival and departure of the
148
- vehicle at the terminus, the vehicle will not be able to charge at the terminus.
149
-
150
- :return: Nothing. The results are added to the database.
152
+ Run a simple consumption simulation and optionally initialize vehicles in the database.
153
+
154
+ This function calculates energy consumption by multiplying each vehicle's total traveled
155
+ distance by a constant ``VehicleType.consumption`` (kWh per km), then updates the database
156
+ with the resulting SoC (State of Charge) data. The function can also use precomputed results
157
+ for specific trips via the ``consumption_result`` parameter.
158
+
159
+ If ``initialize_vehicles`` is True, vehicles and an initial STANDBY event (with 100% SoC)
160
+ are created for each rotation that does not already have a vehicle. If it is False, existing
161
+ vehicles in the database are assumed, and a check is performed to ensure each rotation has a
162
+ vehicle.
163
+
164
+ Opportunity charging can optionally be applied at the end of each trip, if the vehicle and
165
+ station both allow it, and if the rotation is flagged to allow it. This charging event is
166
+ constrained by a configurable terminus deadtime.
167
+
168
+ **SoC Constraints**
169
+
170
+ - When no precomputed results are provided, SoC is computed by subtracting energy used
171
+ (`consumption * distance / battery_capacity`) from the previous event’s SoC.
172
+ - When precomputed ``ConsumptionResult`` objects are provided in ``consumption_result``,
173
+ they must have a non-positive total change in SoC (``delta_soc_total <= 0``).
174
+ If the function detects a positive ``delta_soc_total``, it raises a ``ValueError``.
175
+
176
+ **Timeseries Calculation**
177
+
178
+ - If ``calculate_timeseries`` is True, the function builds a more granular SoC timeseries
179
+ at each stop in the trip and stores it in the ``Event.timeseries`` column.
180
+ - If False, the event’s ``timeseries`` is set to ``None``, which may speed up the simulation
181
+ if you do not need intermediate SoC data.
182
+
183
+ :param scenario:
184
+ One of:
185
+ - A :class:`eflips.model.Scenario` instance containing the input data for the simulation.
186
+ - An integer specifying the ID of a scenario in the database.
187
+ - Any other object with an integer ``id`` attribute.
188
+
189
+ If not passing a :class:`eflips.model.Scenario` directly, the `database_url` parameter
190
+ or the environment variable ``DATABASE_URL`` must point to a valid database.
191
+
192
+ :param initialize_vehicles:
193
+ A boolean flag indicating whether new vehicles should be created and assigned
194
+ to rotations in the database. Set this to True the first time you run the simulation
195
+ so that vehicles are initialized. In subsequent runs, set to False if vehicles
196
+ are already present.
197
+
198
+ :param database_url:
199
+ A database connection string (e.g., ``postgresql://user:pass@host/db``).
200
+ If you do not provide this and ``scenario`` is not a
201
+ :class:`eflips.model.Scenario` instance, the environment variable
202
+ ``DATABASE_URL`` must be set.
203
+
204
+ :param calculate_timeseries:
205
+ If True, each trip’s detailed SoC timeseries is computed and stored in the
206
+ ``timeseries`` column of the corresponding driving and charging events.
207
+ If False, only the start/end SoC is recorded, and ``timeseries`` is set to None.
208
+
209
+ :param terminus_deadtime:
210
+ The total time overhead (attach + detach) for charging at the terminus.
211
+ If this deadtime exceeds the available layover time, no charging is performed.
212
+
213
+ :param consumption_result:
214
+ A dictionary mapping trip IDs to :class:`ConsumptionResult` instances for
215
+ precomputed SoC changes. If an entry exists for a trip, this function uses
216
+ those precomputed SoC changes instead of calculating them from distance
217
+ and consumption. Each ``ConsumptionResult`` must have:
218
+
219
+ - A non-positive ``delta_soc_total`` (<= 0).
220
+ - Optionally, matching lists of timestamps and delta SoC values that are
221
+ decreasing (i.e., the vehicle only loses or maintains SoC).
222
+
223
+ :returns:
224
+ ``None``. All simulation results are written directly to the database as
225
+ :class:`eflips.model.Event` entries.
226
+
227
+ :raises ValueError:
228
+ - If a rotation in the scenario does not have a vehicle when
229
+ ``initialize_vehicles=False``.
230
+ - If the vehicle type has no ``consumption`` value.
231
+ - If a provided ``ConsumptionResult`` has inconsistent list lengths,
232
+ or if its ``delta_soc_total`` is positive.
233
+ - If SoC timeseries are not decreasing when provided
234
+ via ``consumption_result``.
151
235
  """
152
236
  logger = logging.getLogger(__name__)
153
237
 
@@ -211,7 +295,7 @@ def simple_consumption_simulation(
211
295
  area = (
212
296
  session.query(Area)
213
297
  .filter(Area.scenario_id == scenario.id)
214
- .filter(Area.vehicle_type_id == Vehicle.vehicle_type_id)
298
+ .filter(Area.vehicle_type_id == vehicle.vehicle_type_id)
215
299
  .first()
216
300
  )
217
301
 
@@ -253,9 +337,15 @@ def simple_consumption_simulation(
253
337
  .one()
254
338
  )
255
339
  if vehicle_type.consumption is None:
256
- raise ValueError(
257
- "The vehicle type does not have a consumption value set."
258
- )
340
+ # If the vehicle type has no consumption value, all trips must have a precomputed consumption result
341
+ all_trip_ids = [trip.id for trip in rotation.trips]
342
+ if not (
343
+ consumption_result is not None
344
+ and all(trip_id in consumption_result for trip_id in all_trip_ids)
345
+ ):
346
+ raise ValueError(
347
+ "The vehicle type does not have a consumption value set and no consumption results are provided."
348
+ )
259
349
  consumption = vehicle_type.consumption
260
350
 
261
351
  # The departure SoC for this rotation is the SoC of the last event preceding the first trip
@@ -270,36 +360,79 @@ def simple_consumption_simulation(
270
360
 
271
361
  for trip in rotation.trips:
272
362
  # Set up a timeseries
273
- soc_start = current_soc
274
- if calculate_timeseries and len(trip.stop_times) > 0:
275
- timeseries = {
276
- "time": [],
277
- "soc": [],
278
- "distance": [],
279
- }
280
- for i in range(len(trip.stop_times)):
281
- current_time = trip.stop_times[i].arrival_time
282
- dwell_duration = trip.stop_times[i].dwell_duration
283
- elapsed_distance = trip.route.assoc_route_stations[
284
- i
285
- ].elapsed_distance
286
- elapsed_energy = consumption * (elapsed_distance / 1000) # kWh
287
- soc = (
288
- current_soc - elapsed_energy / vehicle_type.battery_capacity
289
- )
290
- timeseries["time"].append(current_time.isoformat())
291
- timeseries["soc"].append(soc)
292
- timeseries["distance"].append(elapsed_distance)
293
- if dwell_duration > timedelta(seconds=0):
294
- timeseries["time"].append(
295
- (current_time + dwell_duration).isoformat()
363
+ if consumption_result is None or trip.id not in consumption_result:
364
+ logger.info("Calculating timeseries for trip %s", trip.id)
365
+ soc_start = current_soc
366
+ if calculate_timeseries and len(trip.stop_times) > 0:
367
+ timeseries = {
368
+ "time": [],
369
+ "soc": [],
370
+ "distance": [],
371
+ }
372
+ for i in range(len(trip.stop_times)):
373
+ current_time = trip.stop_times[i].arrival_time
374
+ dwell_duration = trip.stop_times[i].dwell_duration
375
+ elapsed_distance = trip.route.assoc_route_stations[
376
+ i
377
+ ].elapsed_distance
378
+ elapsed_energy = consumption * (
379
+ elapsed_distance / 1000
380
+ ) # kWh
381
+ soc = (
382
+ current_soc
383
+ - elapsed_energy / vehicle_type.battery_capacity
296
384
  )
385
+ timeseries["time"].append(current_time.isoformat())
297
386
  timeseries["soc"].append(soc)
298
387
  timeseries["distance"].append(elapsed_distance)
388
+ if dwell_duration > timedelta(seconds=0):
389
+ timeseries["time"].append(
390
+ (current_time + dwell_duration).isoformat()
391
+ )
392
+ timeseries["soc"].append(soc)
393
+ timeseries["distance"].append(elapsed_distance)
394
+ else:
395
+ timeseries = None
396
+ energy_used = consumption * trip.route.distance / 1000 # kWh
397
+ current_soc = (
398
+ soc_start - energy_used / vehicle_type.battery_capacity
399
+ )
299
400
  else:
300
- timeseries = None
301
- energy_used = consumption * trip.route.distance / 1000 # kWh
302
- current_soc = soc_start - energy_used / vehicle_type.battery_capacity
401
+ logger.info(f"Using pre-calculated timeseries for trip {trip.id}")
402
+ if (
403
+ calculate_timeseries
404
+ and consumption_result[trip.id].timestamps is not None
405
+ ):
406
+ assert consumption_result[trip.id].delta_soc is not None
407
+ timestamps = consumption_result[trip.id].timestamps
408
+
409
+ # Make sure the delta_soc is a monotonic decreasing function, with the same length as timestamps
410
+ if len(consumption_result[trip.id].delta_soc) != len(
411
+ timestamps
412
+ ):
413
+ raise ValueError(
414
+ "The length of the delta_soc and timestamps lists must be the same."
415
+ )
416
+ delta_socs = consumption_result[trip.id].delta_soc
417
+ if delta_socs[-1] > 0:
418
+ raise ValueError(
419
+ "The delta_soc must be a decreasing function."
420
+ )
421
+
422
+ socs = [current_soc + d for d in delta_socs]
423
+ timeseries = {
424
+ "time": [t.isoformat() for t in timestamps],
425
+ "soc": socs,
426
+ }
427
+ else:
428
+ timeseries = None
429
+
430
+ if consumption_result[trip.id].delta_soc_total > 0:
431
+ raise ValueError(
432
+ "The current SoC must be <= 0 when using a consumption result."
433
+ )
434
+ soc_start = current_soc
435
+ current_soc += consumption_result[trip.id].delta_soc_total
303
436
 
304
437
  # Create a driving event
305
438
  current_event = Event(
@@ -356,6 +489,35 @@ def simple_consumption_simulation(
356
489
  1,
357
490
  )
358
491
 
492
+ # If the post_charge_soc is 1, calculate when the vehicle was full
493
+ if post_charge_soc == 1:
494
+ # 1. Get the max charging power (kW)
495
+ max_power = max([v[1] for v in vehicle_type.charging_curve])
496
+
497
+ # 2. Energy needed (kWh) to go from current_soc to 100%
498
+ energy_needed_kWh = (
499
+ 1 - current_soc
500
+ ) * vehicle_type.battery_capacity
501
+
502
+ # 3. Compute how long that takes at max_power (in hours)
503
+ time_needed_hours = energy_needed_kWh / max_power
504
+
505
+ # 4. Calculate the point in time the vehicle became full
506
+ # If charging effectively starts right after terminus_deadtime
507
+ time_full = (
508
+ trip.arrival_time
509
+ + terminus_deadtime / 2
510
+ + timedelta(hours=time_needed_hours)
511
+ )
512
+
513
+ # 5. Make sure it is before the time charging must end the latest
514
+ assert time_full <= next_trip.departure_time - (
515
+ terminus_deadtime / 2
516
+ )
517
+
518
+ else:
519
+ time_full = None
520
+
359
521
  # Create a simple timeseries for the charging event
360
522
  timeseries = {
361
523
  "time": [
@@ -374,6 +536,11 @@ def simple_consumption_simulation(
374
536
  ],
375
537
  }
376
538
 
539
+ # If time_full is not None, add it to the timeseries in the middle
540
+ if time_full is not None:
541
+ timeseries["time"].insert(2, time_full.isoformat())
542
+ timeseries["soc"].insert(2, 1)
543
+
377
544
  # Create the charging event
378
545
  current_event = Event(
379
546
  scenario_id=scenario.id,
@@ -555,22 +722,23 @@ def simulate_scenario(
555
722
  repetition_period: Optional[timedelta] = None,
556
723
  database_url: Optional[str] = None,
557
724
  smart_charging_strategy: SmartChargingStrategy = SmartChargingStrategy.EVEN,
725
+ ignore_unstable_simulation: bool = False,
558
726
  ) -> None:
559
727
  """
560
728
  This method simulates a scenario and adds the results to the database.
561
729
 
562
730
  It fills in the "Charging Events" in the :class:`eflips.model.Event` table and associates
563
731
  :class:`eflips.model.Vehicle` objects with all the existing "Driving Events" in the :class:`eflips.model.Event`
564
- table.
732
+ table. If the simulation becomes unstable, an :class:`UnstableSimulationException` is raised.
565
733
 
566
734
  :param scenario: Either a :class:`eflips.model.Scenario` object containing the input data for the simulation. Or
567
735
  an integer specifying the ID of a scenario in the database. Or any other object that has an attribute
568
736
  ``id`` that is an integer. If no :class:`eflips.model.Scenario` object is passed, the ``database_url``
569
- parameter must be set to a valid database URL ot the environment variable ``DATABASE_URL`` must be set to a
737
+ parameter must be set to a valid database URL or the environment variable ``DATABASE_URL`` must be set to a
570
738
  valid database URL.
571
739
 
572
740
  :param repetition_period: An optional timedelta object specifying the period of the vehicle schedules. This
573
- is needed because the result should be a steady-state result. THis can only be achieved by simulating a
741
+ is needed because the result should be a steady-state result. This can only be achieved by simulating a
574
742
  time period before and after our actual simulation, and then only using the "middle". eFLIPS tries to
575
743
  automatically detect whether the schedule should be repeated daily or weekly. If this fails, a ValueError is
576
744
  raised and repetition needs to be specified manually.
@@ -580,14 +748,22 @@ def simulate_scenario(
580
748
  URL.
581
749
 
582
750
  :param smart_charging_strategy: An optional parameter specifying the smart charging strategy to be used. The
583
- default is SmartChargingStragegy.NONE. The following strategies are available:
584
- - SmartChargingStragegy.NONE: Do not use smart charging. Buses are charged with the maximum power available,
751
+ default is SmartChargingStrategy.NONE. The following strategies are available:
752
+ - SmartChargingStrategy.NONE: Do not use smart charging. Buses are charged with the maximum power available,
585
753
  from the time they arrive at the depot until they are full (or leave the depot).
586
- - SmartChargingStragegy.EVEN: Use smart charging with an even distribution of charging power over the time the
754
+ - SmartChargingStrategy.EVEN: Use smart charging with an even distribution of charging power over the time the
587
755
  bus is at the depot. This aims to minimize the peak power demand.
756
+ - SmartChargingStrategy.MIN_PRICE: Not implemented yet.
757
+
758
+ :param ignore_unstable_simulation: If True, the simulation will not raise an exception if it becomes unstable.
588
759
 
589
760
  :return: Nothing. The results are added to the database.
761
+
762
+ :raises UnstableSimulationException: If the simulation becomes numerically unstable or if
763
+ the parameters cause the solver to diverge.
590
764
  """
765
+ logger = logging.getLogger(__name__)
766
+
591
767
  with create_session(scenario, database_url) as (session, scenario):
592
768
  simulation_host = init_simulation(
593
769
  scenario=scenario,
@@ -595,7 +771,13 @@ def simulate_scenario(
595
771
  repetition_period=repetition_period,
596
772
  )
597
773
  ev = run_simulation(simulation_host)
598
- add_evaluation_to_database(scenario, ev, session)
774
+ try:
775
+ add_evaluation_to_database(scenario, ev, session)
776
+ except eflips.depot.UnstableSimulationException as e:
777
+ if ignore_unstable_simulation:
778
+ logger.warning("Simulation is unstable. Continuing.")
779
+ else:
780
+ raise e
599
781
 
600
782
  match smart_charging_strategy:
601
783
  case SmartChargingStrategy.NONE:
@@ -842,7 +1024,7 @@ def run_simulation(simulation_host: SimulationHost) -> Dict[str, DepotEvaluation
842
1024
 
843
1025
 
844
1026
  def insert_dummy_standby_departure_events(
845
- depot_id: int, session: Session, sim_time_end: Optional[datetime.datetime] = None
1027
+ depot_id: int, session: Session, sim_time_end: Optional[datetime] = None
846
1028
  ) -> None:
847
1029
  """
848
1030
  Workaround for the missing STANDBY_DEPARTURE events in the database.
@@ -934,6 +1116,9 @@ def add_evaluation_to_database(
934
1116
  database.
935
1117
 
936
1118
  :return: Nothing. The results are added to the database.
1119
+
1120
+ :raises UnstableSimulationException: If the simulation becomes numerically unstable or if
1121
+ the parameters cause the solver to diverge.
937
1122
  """
938
1123
 
939
1124
  # Read simulation start time
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "eflips-depot"
3
- version = "4.4.3"
3
+ version = "4.5.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