eflips-depot 4.14.3__tar.gz → 4.15.9__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.14.3 → eflips_depot-4.15.9}/PKG-INFO +3 -3
  2. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/README.md +1 -1
  3. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/api/__init__.py +19 -1
  4. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/api/private/consumption.py +65 -39
  5. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/api/private/depot.py +91 -25
  6. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/api/private/results_to_database.py +74 -24
  7. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/api/private/util.py +19 -6
  8. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/depot.py +2 -0
  9. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/processes.py +39 -3
  10. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/rating.py +1 -1
  11. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/simple_vehicle.py +12 -1
  12. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/pyproject.toml +2 -2
  13. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/LICENSE.md +0 -0
  14. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/__init__.py +0 -0
  15. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/api/defaults/default_settings.json +0 -0
  16. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/api/private/__init__.py +0 -0
  17. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/configuration.py +0 -0
  18. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/evaluation.py +0 -0
  19. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/filters.py +0 -0
  20. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/input_epex_power_price.py +0 -0
  21. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/__init__.py +0 -0
  22. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/doc/__init__.py +0 -0
  23. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/doc/direct_details.pdf +0 -0
  24. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/evaluation.py +0 -0
  25. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/opt_tools/__init__.py +0 -0
  26. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/opt_tools/crossover.py +0 -0
  27. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/opt_tools/fitness_c_urfd.py +0 -0
  28. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/opt_tools/fitness_util.py +0 -0
  29. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/opt_tools/init.py +0 -0
  30. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/opt_tools/mutation.py +0 -0
  31. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/optimize_c_urfd.py +0 -0
  32. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/packing.py +0 -0
  33. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/settings.py +0 -0
  34. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/template_creation.py +0 -0
  35. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/layout_opt/util.py +0 -0
  36. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/plots.py +0 -0
  37. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/resources.py +0 -0
  38. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/settings_config.py +0 -0
  39. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/simulation.py +0 -0
  40. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/smart_charging.py +0 -0
  41. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/standalone.py +0 -0
  42. {eflips_depot-4.14.3 → eflips_depot-4.15.9}/eflips/depot/validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eflips-depot
3
- Version: 4.14.3
3
+ Version: 4.15.9
4
4
  Summary: Depot Simulation for eFLIPS
5
5
  License: AGPL-3.0-or-later
6
6
  License-File: LICENSE.md
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Requires-Dist: eflips (>=0.1.3,<0.2.0)
16
16
  Requires-Dist: eflips-model (>=10.0.0,<11.0.0)
17
- Requires-Dist: eflips-opt (>=0.3.0,<0.4.0)
17
+ Requires-Dist: eflips-opt (>=0.3.6,<0.4.0)
18
18
  Requires-Dist: pandas (>=2.2.0,<3.0.0)
19
19
  Requires-Dist: scipy (>=1.14.0,<2.0.0)
20
20
  Requires-Dist: simpy (>=4.0.1,<5.0.0)
@@ -131,5 +131,5 @@ This project is licensed under the AGPLv3 license - see the [LICENSE](LICENSE.md
131
131
 
132
132
  ## Funding Notice
133
133
 
134
- This code was developed as part of the project [eBus2030+]([https://www.eflip.de/](https://www.now-gmbh.de/projektfinder/e-bus-2030/)) funded by the Federal German Ministry for Digital and Transport (BMDV) under grant number 03EMF0402.
134
+ This code was developed as part of the project [eBus2030+](https://www.now-gmbh.de/projektfinder/e-bus-2030/) funded by the Federal German Ministry for Digital and Transport (BMDV) under grant number 03EMF0402.
135
135
 
@@ -104,4 +104,4 @@ This project is licensed under the AGPLv3 license - see the [LICENSE](LICENSE.md
104
104
 
105
105
  ## Funding Notice
106
106
 
107
- This code was developed as part of the project [eBus2030+]([https://www.eflip.de/](https://www.now-gmbh.de/projektfinder/e-bus-2030/)) funded by the Federal German Ministry for Digital and Transport (BMDV) under grant number 03EMF0402.
107
+ This code was developed as part of the project [eBus2030+](https://www.now-gmbh.de/projektfinder/e-bus-2030/) funded by the Federal German Ministry for Digital and Transport (BMDV) under grant number 03EMF0402.
@@ -532,6 +532,7 @@ def apply_even_smart_charging(
532
532
  scenario: Union[Scenario, int, Any],
533
533
  database_url: Optional[str] = None,
534
534
  standby_departure_duration: timedelta = timedelta(minutes=5),
535
+ delete_existing_charging_timeseries: bool = False,
535
536
  ) -> None:
536
537
  """
537
538
  Takes a scenario where depot simulation has been run and applies smart charging to the depot.
@@ -548,6 +549,8 @@ def apply_even_smart_charging(
548
549
  :param standby_departure_duration: The duration of the STANDBY_DEPARTURE event. This is the time the vehicle is
549
550
  allowed to wait at the depot before it has to leave. The default is 5 minutes.
550
551
 
552
+ :param delete_existing_timeseries: If True, the existing timeseries in the charging events will be deleted.
553
+
551
554
  :return: None. The results are added to the database.
552
555
  """
553
556
  logger = logging.getLogger(__name__)
@@ -564,6 +567,19 @@ def apply_even_smart_charging(
564
567
  raise
565
568
 
566
569
  with create_session(scenario, database_url) as (session, scenario):
570
+ if delete_existing_charging_timeseries is False:
571
+ raise ValueError(
572
+ "The existing timeseries of charging events needed to be deleted. Set "
573
+ "delete_existing_charging_timeseries=True to delete them."
574
+ )
575
+
576
+ # Delete existing timeseries in charging events
577
+ session.query(Event).filter(
578
+ Event.event_type == EventType.CHARGING_DEPOT,
579
+ Event.scenario_id == scenario.id,
580
+ ).update({"timeseries": None}, synchronize_session=False)
581
+ session.expire_all()
582
+
567
583
  depots = session.query(Depot).filter(Depot.scenario_id == scenario.id).all()
568
584
  for depot in depots:
569
585
  add_slack_time_to_events_of_depot(
@@ -669,7 +685,9 @@ def simulate_scenario(
669
685
  case SmartChargingStrategy.NONE:
670
686
  pass
671
687
  case SmartChargingStrategy.EVEN:
672
- apply_even_smart_charging(scenario, database_url)
688
+ apply_even_smart_charging(
689
+ scenario, database_url, delete_existing_charging_timeseries=True
690
+ )
673
691
  case SmartChargingStrategy.MIN_PRICE:
674
692
  raise NotImplementedError("MIN_PRICE strategy is not implemented yet.")
675
693
  case _:
@@ -3,7 +3,7 @@ import warnings
3
3
  from dataclasses import dataclass
4
4
  from datetime import timedelta, datetime
5
5
  from math import ceil
6
- from typing import Tuple, List
6
+ from typing import Tuple, List, Optional
7
7
  from zoneinfo import ZoneInfo
8
8
 
9
9
  import numpy as np
@@ -88,11 +88,13 @@ class ConsumptionInformation:
88
88
  """
89
89
 
90
90
  trip_id: int
91
- consumption_lut: ConsumptionLut | None # the LUT for the vehicle class
91
+ consumption_lut: Optional[ConsumptionLut] # the LUT for the vehicle class
92
92
  average_speed: float # the average speed of the trip in km/h
93
93
  distance: float # the distance of the trip in km
94
- temperature: float # The ambient temperature in °C
95
- level_of_loading: float
94
+ temperature: Optional[float] # The ambient temperature in °C
95
+ level_of_loading: Optional[
96
+ float
97
+ ] # The level of loading of the vehicle as a fraction of its maximum payload
96
98
  incline: float = 0.0 # The incline of the trip in 0.0-1.0
97
99
  consumption: float = None # The consumption of the trip in kWh
98
100
  consumption_per_km: float = None # The consumption per km in kWh
@@ -286,54 +288,78 @@ def extract_trip_information(
286
288
  )
287
289
  .one()
288
290
  )
291
+
292
+ total_distance = trip.route.distance / 1000 # km
293
+ total_duration = (
294
+ trip.arrival_time - trip.departure_time
295
+ ).total_seconds() / 3600
296
+ average_speed = total_distance / total_duration # km/h
297
+
289
298
  # Check exactly one of the vehicle classes has a consumption LUT
290
299
  all_consumption_luts = [
291
300
  vehicle_class.consumption_lut
292
301
  for vehicle_class in trip.rotation.vehicle_type.vehicle_classes
293
302
  ]
294
303
  all_consumption_luts = [x for x in all_consumption_luts if x is not None]
295
- if len(all_consumption_luts) != 1:
296
- raise ValueError(
297
- f"Expected exactly one consumption LUT, got {len(all_consumption_luts)}"
298
- )
299
- consumption_lut = all_consumption_luts[0]
300
- # Disconnect the consumption LUT from the session to avoid loading the whole table
301
304
 
302
- del all_consumption_luts
305
+ temperature = temperature_for_trip(trip_id, session)
303
306
 
304
- total_distance = trip.route.distance / 1000 # km
305
- total_duration = (
306
- trip.arrival_time - trip.departure_time
307
- ).total_seconds() / 3600
308
- average_speed = total_distance / total_duration # km/h
307
+ if len(all_consumption_luts) == 1:
308
+ payload_mass = passenger_mass * passenger_count
309
+ assert (
310
+ trip.rotation.vehicle_type.allowed_mass is not None
311
+ ), f"allowed_mass of vehicle {trip.rotation.vehicle_type} must be set"
309
312
 
310
- temperature = temperature_for_trip(trip_id, session)
313
+ assert (
314
+ trip.rotation.vehicle_type.empty_mass is not None
315
+ ), f"empty_mass of vehicle {trip.rotation.vehicle_type} must be set"
311
316
 
312
- payload_mass = passenger_mass * passenger_count
313
- assert (
314
- trip.rotation.vehicle_type.allowed_mass is not None
315
- ), f"allowed_mass of vehicle {trip.rotation.vehicle_type} must be set"
317
+ full_payload = (
318
+ trip.rotation.vehicle_type.allowed_mass
319
+ - trip.rotation.vehicle_type.empty_mass
320
+ )
321
+ level_of_loading = payload_mass / full_payload
316
322
 
317
- assert (
318
- trip.rotation.vehicle_type.empty_mass is not None
319
- ), f"empty_mass of vehicle {trip.rotation.vehicle_type} must be set"
323
+ consumption_lut = all_consumption_luts[0]
324
+ # Disconnect the consumption LUT from the session to avoid loading the whole table
320
325
 
321
- full_payload = (
322
- trip.rotation.vehicle_type.allowed_mass
323
- - trip.rotation.vehicle_type.empty_mass
324
- )
325
- level_of_loading = payload_mass / full_payload
326
-
327
- info = ConsumptionInformation(
328
- trip_id=trip.id,
329
- consumption_lut=consumption_lut,
330
- average_speed=average_speed,
331
- distance=total_distance,
332
- temperature=temperature,
333
- level_of_loading=level_of_loading,
334
- )
326
+ del all_consumption_luts
327
+
328
+ info = ConsumptionInformation(
329
+ trip_id=trip.id,
330
+ consumption_lut=consumption_lut,
331
+ average_speed=average_speed,
332
+ distance=total_distance,
333
+ temperature=temperature,
334
+ level_of_loading=level_of_loading,
335
+ )
336
+ info.calculate()
337
+ elif len(all_consumption_luts) == 0:
338
+ warnings.warn(
339
+ f"No consumption LUT found for vehicle type {trip.rotation.vehicle_type}.",
340
+ ConsistencyWarning,
341
+ )
342
+ # Here, we fill out the condumption information without the LUT and LUT data, but with `consumption_per_km`
343
+ # set to the vehicle's `consumption` value.
344
+ if trip.rotation.vehicle_type.consumption is None:
345
+ raise ValueError(
346
+ f"Vehicle type {trip.rotation.vehicle_type} must have a consumption value set if no consumption LUT is available."
347
+ )
348
+ info = ConsumptionInformation(
349
+ trip_id=trip.id,
350
+ average_speed=average_speed,
351
+ distance=total_distance,
352
+ consumption_per_km=trip.rotation.vehicle_type.consumption,
353
+ consumption=trip.rotation.vehicle_type.consumption * total_distance,
354
+ consumption_lut=None,
355
+ temperature=temperature,
356
+ level_of_loading=None,
357
+ )
358
+ else:
359
+ raise ValueError(
360
+ f"Expected exactly one consumption LUT, got {len(all_consumption_luts)}"
361
+ )
335
362
 
336
- info.calculate()
337
363
  return info
338
364
 
339
365
 
@@ -126,20 +126,27 @@ class DepotConfigurationWish:
126
126
  self.areas = areas
127
127
 
128
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
129
+ assert (
130
+ self.default_power is not None
131
+ ), "default_power must be provided if auto_generate is True"
132
+ assert (
133
+ self.standard_block_length is not None
134
+ ), "standard_block_length must be provided if auto_generate is True"
135
+
136
+ assert (
137
+ self.cleaning_slots is None
138
+ ), "cleaning_slots cannot be provided if auto_generate is True"
139
+ assert (
140
+ self.cleaning_duration is None
141
+ ), "cleaning_duration cannot be provided if auto_generate is True"
142
+ assert (
143
+ self.shunting_slots is None
144
+ ), "shunting_slots cannot be provided if auto_generate is True"
145
+ assert (
146
+ self.shunting_duration is None
147
+ ), "shunting_duration cannot be provided if auto_generate is True"
148
+
149
+ assert areas is None, "areas cannot be provided if auto_generate is True"
143
150
 
144
151
 
145
152
  def delete_depots(scenario: Scenario, session: Session) -> None:
@@ -204,7 +211,7 @@ def depot_to_template(depot: Depot) -> Dict[str, str | Dict[str, str | int]]:
204
211
  list_of_processes = []
205
212
 
206
213
  # Get dictionary of each area
207
- # For line areas, generate an dictionary item for total areas, later it will be split into individual lines
214
+ # For line areas, generate a dictionary item for total areas, later it will be split into individual lines
208
215
  for area in depot.areas:
209
216
  area_name = str(area.id)
210
217
  template["areas"][area_name] = {
@@ -226,6 +233,17 @@ def depot_to_template(depot: Depot) -> Dict[str, str | Dict[str, str | int]]:
226
233
  "filter_names": ["vehicle_type"],
227
234
  "vehicle_types": [str(area.vehicle_type_id)],
228
235
  }
236
+ for processes_in_area in area.processes:
237
+ if process_type(processes_in_area) == ProcessType.CHARGING:
238
+ # Add the charging process for this vehicle type
239
+ template["areas"][area_name]["available_processes"].append(
240
+ str(processes_in_area.id) + "vt" + str(area.vehicle_type_id)
241
+ )
242
+ # Delete the original charging process
243
+ template["areas"][area_name]["available_processes"].remove(
244
+ str(processes_in_area.id)
245
+ )
246
+
229
247
  else:
230
248
  # If the vehicle type id is not set, the area is for all vehicle types
231
249
  scenario = depot.scenario
@@ -236,6 +254,19 @@ def depot_to_template(depot: Depot) -> Dict[str, str | Dict[str, str | int]]:
236
254
  "vehicle_types": all_vehicle_type_ids,
237
255
  }
238
256
 
257
+ # if there are any charging areas, all a unique charging process for each vehicle type
258
+ for processes_in_area in area.processes:
259
+ if process_type(processes_in_area) == ProcessType.CHARGING:
260
+ # Add the charging process for this vehicle type
261
+ for vt in scenario.vehicle_types:
262
+ template["areas"][area_name]["available_processes"].append(
263
+ str(processes_in_area.id) + "vt" + str(vt.id)
264
+ )
265
+ # Delete the original charging process
266
+ template["areas"][area_name]["available_processes"].remove(
267
+ str(processes_in_area.id)
268
+ )
269
+
239
270
  for process in area.processes:
240
271
  # Add process into process list
241
272
  list_of_processes.append(
@@ -285,7 +316,7 @@ def depot_to_template(depot: Depot) -> Dict[str, str | Dict[str, str | int]]:
285
316
  for name in line_areas_to_delete:
286
317
  del area_template[name]
287
318
 
288
- # Fill in the dictionary of processess
319
+ # Fill in the dictionary of processes
289
320
  for process in list_of_processes:
290
321
  process_name = str(process.id)
291
322
  # Shared template for all processes
@@ -358,8 +389,43 @@ def depot_to_template(depot: Depot) -> Dict[str, str | Dict[str, str | int]]:
358
389
  ] = list_of_breaks_in_seconds
359
390
 
360
391
  case ProcessType.CHARGING:
361
- template["processes"][process_name]["typename"] = "Charge"
362
- del template["processes"][process_name]["dur"]
392
+ all_vehicle_types = depot.scenario.vehicle_types
393
+
394
+ for vt in all_vehicle_types:
395
+ charging_curve = vt.charging_curve
396
+
397
+ charging_process_name = process_name + "vt" + str(vt.id)
398
+ template["processes"][charging_process_name] = template[
399
+ "processes"
400
+ ][process_name].copy()
401
+ template["processes"][charging_process_name][
402
+ "typename"
403
+ ] = "ChargeEquationSteps"
404
+ template["processes"][charging_process_name]["vehicle_filter"] = {
405
+ "filter_names": ["vehicle_type"],
406
+ "vehicle_types": [str(vt.id)],
407
+ }
408
+
409
+ template["processes"][charging_process_name][
410
+ "peq_name"
411
+ ] = "charging_curve_power"
412
+ template["processes"][charging_process_name]["peq_params"] = {
413
+ "soc": [soc_power_pair[0] for soc_power_pair in charging_curve],
414
+ "power": [
415
+ soc_power_pair[1] for soc_power_pair in charging_curve
416
+ ],
417
+ "precision": 0.01,
418
+ }
419
+
420
+ del template["processes"][charging_process_name]["dur"]
421
+
422
+ # delete the original process
423
+ del template["processes"][process_name]
424
+
425
+ # The original one
426
+ # template["processes"][process_name]["typename"] = "Charge"
427
+
428
+ # del template["processes"][process_name]["dur"]
363
429
 
364
430
  case ProcessType.STANDBY | ProcessType.STANDBY_DEPARTURE:
365
431
  template["processes"][process_name]["typename"] = "Standby"
@@ -1137,7 +1203,9 @@ def depot_smallest_possible_size(
1137
1203
 
1138
1204
  # Simulate the depot
1139
1205
  simulate_scenario(
1140
- scenario, smart_charging_strategy=SmartChargingStrategy.NONE
1206
+ scenario,
1207
+ smart_charging_strategy=SmartChargingStrategy.NONE,
1208
+ ignore_unstable_simulation=True,
1141
1209
  )
1142
1210
 
1143
1211
  # Find the peak usage of the depot
@@ -1201,12 +1269,6 @@ def depot_smallest_possible_size(
1201
1269
 
1202
1270
  except MissingVehicleDimensionError as e:
1203
1271
  raise e
1204
- except UnstableSimulationException as e:
1205
- # This change is made after Unstable exception and delay exceptions are introduced
1206
- logger.debug(
1207
- f"Results are unstable, suggesting depot is too small."
1208
- )
1209
- continue
1210
1272
  except DelayedTripException as e:
1211
1273
  logger.debug(f"Trips are delayed, suggesting depot is too small.")
1212
1274
  continue
@@ -1216,6 +1278,10 @@ def depot_smallest_possible_size(
1216
1278
  # Identify the best configuration for each vehicle type
1217
1279
  ret_val: Dict[VehicleType, Dict[AreaType, int]] = dict()
1218
1280
  for vt in vts_and_rotations.keys():
1281
+ assert area_needed[vt] != {}, (
1282
+ f"No valid configurations found for {vt.name}, "
1283
+ f"please check if there are any delays after simulating scenario."
1284
+ )
1219
1285
  best_config = min(area_needed[vt].keys(), key=lambda x: area_needed[vt][x])
1220
1286
  ret_val[vt] = {
1221
1287
  AreaType.LINE: best_config * standard_block_length,
@@ -1,6 +1,7 @@
1
1
  import datetime
2
2
  import itertools
3
3
  import logging
4
+ import math
4
5
  import warnings
5
6
  from datetime import timedelta
6
7
  from typing import List, Dict
@@ -225,8 +226,6 @@ def generate_vehicle_events(
225
226
  "is_waiting": True,
226
227
  }
227
228
 
228
- # Create a list of battery log in order of time asc. Convenient for looking up corresponding soc
229
-
230
229
  for time_stamp, process_log in current_vehicle.logger.loggedData[
231
230
  "dwd.active_processes_copy"
232
231
  ].items():
@@ -276,13 +275,18 @@ def generate_vehicle_events(
276
275
  )
277
276
  start_this_event = dict_of_events[time_stamp]["end"]
278
277
  else:
279
- for other_process in process_log:
280
- if (
281
- other_process.ID != process.ID
282
- and other_process.dur > 0
283
- ):
284
- start_this_event = other_process.ends[0]
285
- break
278
+ if len(process_log) > 1:
279
+ # This is for the case where the charging and standby_departure happen in the same area, and the standby_departure is the last process.
280
+ for other_process in process_log:
281
+ if (
282
+ other_process.ID != process.ID
283
+ and other_process.dur > 0
284
+ ):
285
+ start_this_event = other_process.ends[0]
286
+ break
287
+ else:
288
+ # This is for the case where only standby_departure happens in the last area.
289
+ start_this_event = time_stamp
286
290
 
287
291
  assert (
288
292
  start_this_event is not None
@@ -395,7 +399,11 @@ def add_soc_to_events(dict_of_events, battery_log) -> None:
395
399
  """
396
400
  battery_log_list = []
397
401
  for log in battery_log:
398
- battery_log_list.append((log.t, log.energy / log.energy_real))
402
+ # TODO this is a bypass of update events having lower energy_real than the event before. It happens in processes L 1304
403
+ if log.event_name == "update":
404
+ continue
405
+
406
+ battery_log_list.append((log.t, round(log.energy / log.energy_real, 4)))
399
407
 
400
408
  time_keys = sorted(dict_of_events.keys())
401
409
 
@@ -408,15 +416,42 @@ def add_soc_to_events(dict_of_events, battery_log) -> None:
408
416
  start_time = time_keys[i]
409
417
  process_dict = dict_of_events[time_keys[i]]
410
418
 
411
- if process_dict["type"] != "Trip":
412
- soc_start = np.interp(start_time, battery_log_times, battery_log_socs)
413
- process_dict["soc_start"] = min(float(soc_start), 1.0)
414
- soc_end = np.interp(
415
- process_dict["end"], battery_log_times, battery_log_socs
416
- )
417
- process_dict["soc_end"] = min(float(soc_end), 1.0)
418
- else:
419
- continue
419
+ match process_dict["type"]:
420
+ case "Charge" | "ChargeSteps" | "ChargeEquationSteps":
421
+ event_start = start_time
422
+ event_end = process_dict["end"]
423
+ start_time_index = battery_log_times.index(event_start)
424
+ end_time_index = battery_log_times.index(event_end)
425
+ time_series = {
426
+ "time": battery_log_times[start_time_index:end_time_index],
427
+ "soc": battery_log_socs[start_time_index:end_time_index],
428
+ }
429
+
430
+ # if there are repeated timestamps, we need to remove them
431
+ unique_indices = np.unique(
432
+ time_series["time"], return_index=True, return_inverse=False
433
+ )[1]
434
+ time_series["time"] = [
435
+ time_series["time"][index] for index in unique_indices
436
+ ]
437
+ time_series["soc"] = [
438
+ time_series["soc"][index] for index in unique_indices
439
+ ]
440
+ process_dict["timeseries"] = time_series
441
+ process_dict["soc_start"] = battery_log_socs[start_time_index]
442
+ process_dict["soc_end"] = battery_log_socs[end_time_index]
443
+
444
+ case "Serve" | "Standby":
445
+ soc_start = np.interp(start_time, battery_log_times, battery_log_socs)
446
+ process_dict["soc_start"] = min(float(soc_start), 1.0)
447
+ soc_end = np.interp(
448
+ process_dict["end"], battery_log_times, battery_log_socs
449
+ )
450
+ process_dict["soc_end"] = min(float(soc_end), 1.0)
451
+ case "Trip":
452
+ continue
453
+ case _:
454
+ raise NotImplementedError
420
455
 
421
456
 
422
457
  def add_events_into_database(
@@ -446,7 +481,7 @@ def add_events_into_database(
446
481
  match process_dict["type"]:
447
482
  case "Serve":
448
483
  event_type = EventType.SERVICE
449
- case "Charge":
484
+ case "Charge" | "ChargeSteps" | "ChargeEquationSteps": # TODO that might be problematic
450
485
  event_type = EventType.CHARGING_DEPOT
451
486
  case "Standby":
452
487
  if (
@@ -466,7 +501,7 @@ def add_events_into_database(
466
501
  '"Standby", "Precondition"'
467
502
  )
468
503
 
469
- if process_dict["end"] == start_time:
504
+ if math.ceil(process_dict["end"]) == math.ceil(start_time):
470
505
  logger.warning("Refusing to create an event with zero duration.")
471
506
  continue
472
507
 
@@ -488,6 +523,20 @@ def add_events_into_database(
488
523
  capacity_per_line = int(current_area.capacity / current_area.row_count)
489
524
  process_dict["slot"] = capacity_per_line * row + process_dict["slot"] - 1
490
525
 
526
+ timeseries = {}
527
+ if "timeseries" in process_dict.keys():
528
+ # Convert "time" in timeseries to timedelta
529
+ timeseries["time"] = [
530
+ (timedelta(seconds=math.ceil(t)) + simulation_start_time).isoformat()
531
+ for t in process_dict["timeseries"]["time"]
532
+ ]
533
+ timeseries["soc"] = process_dict["timeseries"]["soc"]
534
+
535
+ assert all(
536
+ timeseries["soc"][i] <= timeseries["soc"][i + 1]
537
+ for i in range(len(timeseries["soc"]) - 1)
538
+ ), "SOC values in the timeseries should be non-decreasing."
539
+
491
540
  current_event = Event(
492
541
  scenario=scenario,
493
542
  vehicle_type_id=db_vehicle.vehicle_type_id,
@@ -496,8 +545,9 @@ def add_events_into_database(
496
545
  area_id=area_id,
497
546
  subloc_no=process_dict["slot"] if "slot" in process_dict.keys() else 00,
498
547
  trip_id=None,
499
- time_start=timedelta(seconds=start_time) + simulation_start_time,
500
- time_end=timedelta(seconds=process_dict["end"]) + simulation_start_time,
548
+ time_start=timedelta(seconds=math.ceil(start_time)) + simulation_start_time,
549
+ time_end=timedelta(seconds=math.ceil(process_dict["end"]))
550
+ + simulation_start_time,
501
551
  soc_start=process_dict["soc_start"]
502
552
  if process_dict["soc_start"] is not None
503
553
  else process_dict["soc_end"],
@@ -507,7 +557,7 @@ def add_events_into_database(
507
557
  # then this is not an event with soc change
508
558
  event_type=event_type,
509
559
  description=process_dict["id"] if "id" in process_dict.keys() else None,
510
- timeseries=None,
560
+ timeseries=timeseries if "timeseries" in process_dict.keys() else None,
511
561
  )
512
562
 
513
563
  session.add(current_event)
@@ -1,6 +1,7 @@
1
1
  """This module contains miscellaneous utility functions for the eflips-depot API."""
2
2
  import logging
3
3
  import os
4
+ import warnings
4
5
  from contextlib import contextmanager
5
6
  from dataclasses import dataclass
6
7
  from datetime import timedelta, datetime
@@ -17,9 +18,11 @@ from eflips.model import (
17
18
  Trip,
18
19
  Depot,
19
20
  Temperatures,
21
+ ConsistencyWarning,
20
22
  )
21
23
  from eflips.model import create_engine
22
24
  from sqlalchemy import inspect
25
+ from sqlalchemy.exc import NoResultFound
23
26
  from sqlalchemy.orm import Session
24
27
 
25
28
  from eflips.depot import SimpleTrip, Timetable as EflipsTimeTable
@@ -99,6 +102,7 @@ def vehicle_type_to_global_constants_dict(vt: VehicleType) -> Dict[str, float]:
99
102
  "soc_max": 1.0,
100
103
  "soc_init": 1.0,
101
104
  "soh": 1.0,
105
+ "charging_efficiency": vt.charging_efficiency,
102
106
  }
103
107
  return the_dict
104
108
 
@@ -204,7 +208,9 @@ def check_depot_validity(depot: Depot) -> None:
204
208
 
205
209
  def temperature_for_trip(trip_id: int, session: Session) -> float:
206
210
  """
207
- Returns the temperature for a trip. Finds the temperature for the mid-point of the trip.
211
+ Returns the temperature for a trip.
212
+
213
+ Finds the temperature for the mid-point of the trip.
208
214
 
209
215
  :param trip_id: The ID of the trip
210
216
  :param session: The SQLAlchemy session
@@ -212,11 +218,18 @@ def temperature_for_trip(trip_id: int, session: Session) -> float:
212
218
  """
213
219
 
214
220
  trip = session.query(Trip).filter(Trip.id == trip_id).one()
215
- temperatures = (
216
- session.query(Temperatures)
217
- .filter(Temperatures.scenario_id == trip.scenario_id)
218
- .one()
219
- )
221
+ try:
222
+ temperatures = (
223
+ session.query(Temperatures)
224
+ .filter(Temperatures.scenario_id == trip.scenario_id)
225
+ .one()
226
+ )
227
+ except NoResultFound:
228
+ warnings.warn(
229
+ f"No temperatures found for scenario {trip.scenario_id}.",
230
+ ConsistencyWarning,
231
+ )
232
+ return None
220
233
 
221
234
  # Find the mid-point of the trip
222
235
  mid_time = trip.departure_time + (trip.arrival_time - trip.departure_time) / 2
@@ -1360,7 +1360,9 @@ class DepotControl:
1360
1360
  for procID in area.available_processes
1361
1361
  if self.depot.processes[procID]["type"].request_immediately
1362
1362
  and (
1363
+ # TODO: Simplify this condition
1363
1364
  self.depot.processes[procID]["kwargs"]["ismandatory"]
1365
+ and self.depot.processes[procID]["kwargs"]["vehicle_filter"](vehicle)
1364
1366
  or not self.depot.processes[procID]["kwargs"]["ismandatory"]
1365
1367
  and self.depot.processes[procID]["kwargs"]["vehicle_filter"](vehicle)
1366
1368
  )
@@ -6,6 +6,7 @@ from abc import ABC, abstractmethod
6
6
  from enum import auto, Enum
7
7
  from warnings import warn
8
8
 
9
+ import numpy as np
9
10
  import simpy
10
11
  from eflips.helperFunctions import flexprint
11
12
  from eflips.settings import globalConstants
@@ -672,7 +673,7 @@ class ChargeAbstract(VehicleProcess, ABC):
672
673
  cancellable_for_dispatch=False,
673
674
  efficiency=1,
674
675
  *args,
675
- **kwargs
676
+ **kwargs,
676
677
  ):
677
678
  if required_resources is not None:
678
679
  raise ValueError(
@@ -839,6 +840,7 @@ class Charge(ChargeAbstract):
839
840
 
840
841
  @property
841
842
  def power(self):
843
+ # TODO this doesn't consider vehicle charging power
842
844
  return self.charging_interface.max_power
843
845
 
844
846
  def _action(self, *args, **kwargs):
@@ -1239,6 +1241,8 @@ class ChargeEquationSteps(ChargeAbstract):
1239
1241
  self.precision = precision
1240
1242
  Charge.check_soc_target(soc_target, vehicle_filter)
1241
1243
  self.soc_target = soc_target
1244
+ if "precision" in self.peq_params:
1245
+ self.precision = self.peq_params["precision"]
1242
1246
 
1243
1247
  @property
1244
1248
  def power(self):
@@ -1270,8 +1274,12 @@ class ChargeEquationSteps(ChargeAbstract):
1270
1274
  )
1271
1275
  soc_target_step = self.vehicle.battery.soc + soc_interval
1272
1276
  amount = self.vehicle.battery.energy_real * soc_interval
1273
- effective_power = self.power * self.efficiency
1274
- self.dur = int(amount / effective_power * 3600)
1277
+ effective_power = (
1278
+ self.power
1279
+ * self.efficiency
1280
+ * self.vehicle.vehicle_type.charging_efficiency
1281
+ )
1282
+ self.dur = round(amount / effective_power * 3600, 12)
1275
1283
 
1276
1284
  if self.dur == 0:
1277
1285
  # amount is so small that dur is <1s. Reduce the precision
@@ -1294,6 +1302,7 @@ class ChargeEquationSteps(ChargeAbstract):
1294
1302
  yield self.env.timeout(self.dur)
1295
1303
 
1296
1304
  if soc_target_step < self.soc_target:
1305
+ # TODO why here we have energy higher than the step_soc_target?
1297
1306
  # recalculate amount because update_battery may have been
1298
1307
  # called in the meantime
1299
1308
  amount_step = (
@@ -1428,6 +1437,33 @@ def exponential_power(vehicle, charging_interface, peq_params, *args, **kwargs):
1428
1437
  )
1429
1438
 
1430
1439
 
1440
+ def charging_curve_power(vehicle, charging_interface, peq_params):
1441
+ """
1442
+ Return power in kW for given peq_params in the format of dict[flaot, float]
1443
+ :param vehicle: SimpleVehicle
1444
+ :param charging_interface: DepotChargingInterface storing maximum power from charger
1445
+ :param peq_params: A dictionary with "soc" storing soc turning points and "power" for corresponding charging power
1446
+ :return: charging power in float for given SimpleVehicle
1447
+ """
1448
+
1449
+ precision = peq_params.get("precision", 0.01)
1450
+ current_soc = vehicle.battery.soc
1451
+ p_max = charging_interface.max_power
1452
+ target_step_soc = min(vehicle.battery.soc + precision, vehicle.battery.soc_max)
1453
+
1454
+ power_current_soc = min(
1455
+ p_max, np.interp(current_soc, peq_params["soc"], peq_params["power"])
1456
+ )
1457
+
1458
+ power_target_soc = min(
1459
+ p_max, np.interp(target_step_soc, peq_params["soc"], peq_params["power"])
1460
+ )
1461
+
1462
+ current_power = min(power_current_soc, power_target_soc)
1463
+
1464
+ return float(current_power)
1465
+
1466
+
1431
1467
  class Standby(VehicleProcess):
1432
1468
  """Process of mandatory waiting such as standby times."""
1433
1469
 
@@ -422,7 +422,7 @@ class RfdDiffDispatch:
422
422
  diffs = []
423
423
  for vehicle in area.vehicles:
424
424
  etc = vehicle.dwd.etc_processes
425
- if isinstance(etc, int):
425
+ if isinstance(etc, int) or isinstance(etc, float):
426
426
  diff = etc - slot[0].env.now
427
427
  diffs.append(diff)
428
428
  # Convert EstimateValue to numerical rfddiff
@@ -36,7 +36,17 @@ class VehicleType:
36
36
 
37
37
  """
38
38
 
39
- def __init__(self, ID, battery_capacity, soc_min, soc_max, soc_init, soh, CR=None):
39
+ def __init__(
40
+ self,
41
+ ID,
42
+ battery_capacity,
43
+ soc_min,
44
+ soc_max,
45
+ soc_init,
46
+ soh,
47
+ charging_efficiency=1.0,
48
+ CR=None,
49
+ ):
40
50
  self.ID = ID
41
51
  self.battery_capacity = battery_capacity
42
52
  self.soc_min = soc_min
@@ -45,6 +55,7 @@ class VehicleType:
45
55
  self.soh = soh
46
56
  self.CR = CR
47
57
  self.group = None
58
+ self.charging_efficiency = charging_efficiency
48
59
 
49
60
  self.count = {}
50
61
  self.share = {}
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "eflips-depot"
3
- version = "4.14.3"
3
+ version = "4.15.9"
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",
@@ -23,7 +23,7 @@ pandas = "^2.2.0"
23
23
  xlrd = "<=1.2.0"
24
24
  scipy = "^1.14.0"
25
25
  eflips-model = "^10.0.0"
26
- eflips-opt = "^0.3.0"
26
+ eflips-opt = "^0.3.6"
27
27
 
28
28
 
29
29
  [tool.pytest.ini_options]
File without changes