eflips-depot 4.5.0__py3-none-any.whl → 4.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of eflips-depot might be problematic. Click here for more details.

@@ -47,6 +47,7 @@ from eflips.model import (
47
47
  Vehicle,
48
48
  VehicleType,
49
49
  AreaType,
50
+ ChargeType,
50
51
  )
51
52
  from sqlalchemy.orm import Session
52
53
 
@@ -55,6 +56,11 @@ from eflips.depot import (
55
56
  DepotEvaluation,
56
57
  SimulationHost,
57
58
  )
59
+ from eflips.depot.api.private.consumption import (
60
+ initialize_vehicle,
61
+ add_initial_standby_event,
62
+ attempt_opportunity_charging_event,
63
+ )
58
64
  from eflips.depot.api.private.depot import (
59
65
  delete_depots,
60
66
  depot_to_template,
@@ -246,76 +252,18 @@ def simple_consumption_simulation(
246
252
  )
247
253
  if initialize_vehicles:
248
254
  for rotation in rotations:
249
- vehicle = Vehicle(
250
- vehicle_type_id=rotation.vehicle_type_id,
251
- scenario_id=scenario.id,
252
- name=f"Vehicle for rotation {rotation.id}",
253
- )
254
- session.add(vehicle)
255
- rotation.vehicle = vehicle
255
+ initialize_vehicle(rotation, session)
256
256
 
257
- # Additionally, add a short STANDBY event with 100% SoC immediately before the first trip
258
- first_trip_start = rotation.trips[0].departure_time
259
- standby_start = first_trip_start - timedelta(seconds=1)
260
- standby_event = Event(
261
- scenario_id=scenario.id,
262
- vehicle_type_id=rotation.vehicle_type_id,
263
- vehicle=vehicle,
264
- station_id=rotation.trips[0].route.departure_station_id,
265
- subloc_no=0,
266
- time_start=standby_start,
267
- time_end=first_trip_start,
268
- soc_start=1,
269
- soc_end=1,
270
- event_type=EventType.CHARGING_OPPORTUNITY,
271
- description=f"DUMMY Initial standby event for rotation {rotation.id}.",
272
- timeseries=None,
273
- )
274
- session.add(standby_event)
275
- else:
276
- for rotation in rotations:
277
- if rotation.vehicle is None:
278
- raise ValueError(
279
- "The rotation does not have a vehicle assigned to it."
280
- )
281
-
282
- vehicles = (
283
- session.query(Vehicle).filter(Vehicle.scenario_id == scenario.id).all()
284
- )
285
- for vehicle in vehicles:
286
- if (
287
- session.query(Event).filter(Event.vehicle_id == vehicle.id).count()
288
- == 0
289
- ):
290
- # Also add a dummy standby-departure event if this vehicle has no events
291
- rotation_per_vehicle = sorted(
292
- vehicle.rotations, key=lambda r: r.trips[0].departure_time
293
- )
294
- earliest_trip = rotation_per_vehicle[0].trips[0]
295
- area = (
296
- session.query(Area)
297
- .filter(Area.scenario_id == scenario.id)
298
- .filter(Area.vehicle_type_id == vehicle.vehicle_type_id)
299
- .first()
300
- )
257
+ for rotation in rotations:
258
+ if rotation.vehicle is None:
259
+ raise ValueError("The rotation does not have a vehicle assigned to it.")
301
260
 
302
- standby_start = earliest_trip.departure_time - timedelta(seconds=1)
303
- standby_event = Event(
304
- scenario_id=scenario.id,
305
- vehicle_type_id=vehicle.vehicle_type_id,
306
- vehicle=vehicle,
307
- station_id=area.depot.station_id,
308
- area_id=area.id,
309
- subloc_no=area.capacity,
310
- time_start=standby_start,
311
- time_end=earliest_trip.departure_time,
312
- soc_start=1,
313
- soc_end=1,
314
- event_type=EventType.STANDBY_DEPARTURE,
315
- description=f"DUMMY Initial standby event for rotation {earliest_trip.rotation_id}.",
316
- timeseries=None,
317
- )
318
- session.add(standby_event)
261
+ vehicles = (
262
+ session.query(Vehicle).filter(Vehicle.scenario_id == scenario.id).all()
263
+ )
264
+ for vehicle in vehicles:
265
+ if session.query(Event).filter(Event.vehicle_id == vehicle.id).count() == 0:
266
+ add_initial_standby_event(vehicle, session)
319
267
 
320
268
  # Since we are doing no_autoflush blocks later, we need to flush the session once here so that unflushed stuff
321
269
  # From preceding functions is visible in the database
@@ -361,7 +309,7 @@ def simple_consumption_simulation(
361
309
  for trip in rotation.trips:
362
310
  # Set up a timeseries
363
311
  if consumption_result is None or trip.id not in consumption_result:
364
- logger.info("Calculating timeseries for trip %s", trip.id)
312
+ logger.debug("Calculating consumption for trip %s", trip.id)
365
313
  soc_start = current_soc
366
314
  if calculate_timeseries and len(trip.stop_times) > 0:
367
315
  timeseries = {
@@ -398,7 +346,7 @@ def simple_consumption_simulation(
398
346
  soc_start - energy_used / vehicle_type.battery_capacity
399
347
  )
400
348
  else:
401
- logger.info(f"Using pre-calculated timeseries for trip {trip.id}")
349
+ logger.debug(f"Using pre-calculated timeseries for trip {trip.id}")
402
350
  if (
403
351
  calculate_timeseries
404
352
  and consumption_result[trip.id].timestamps is not None
@@ -461,102 +409,20 @@ def simple_consumption_simulation(
461
409
  rotation.vehicle_type.opportunity_charging_capable
462
410
  and rotation.allow_opportunity_charging
463
411
  and trip.route.arrival_station.is_electrified
412
+ and trip.route.arrival_station.charge_type == ChargeType.OPPORTUNITY
464
413
  and trip != rotation.trips[-1]
465
414
  ):
466
- logger.debug(
467
- f"Adding opportunity charging event for trip {trip.id}"
468
- )
469
- # Identify the break time between trips
470
415
  trip_index = rotation.trips.index(trip)
471
416
  next_trip = rotation.trips[trip_index + 1]
472
- break_time = next_trip.departure_time - trip.arrival_time
473
-
474
- # How much energy can be charged in this time?
475
- energy_charged = (
476
- max([v[1] for v in vehicle_type.charging_curve])
477
- * (
478
- break_time.total_seconds()
479
- - terminus_deadtime.total_seconds()
480
- )
481
- / 3600
482
- )
483
-
484
- if energy_charged > 0:
485
- # Calculate the end SoC
486
- post_charge_soc = min(
487
- current_soc
488
- + energy_charged / vehicle_type.battery_capacity,
489
- 1,
490
- )
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
417
 
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
-
521
- # Create a simple timeseries for the charging event
522
- timeseries = {
523
- "time": [
524
- trip.arrival_time.isoformat(),
525
- (trip.arrival_time + terminus_deadtime / 2).isoformat(),
526
- (
527
- next_trip.departure_time - terminus_deadtime / 2
528
- ).isoformat(),
529
- next_trip.departure_time.isoformat(),
530
- ],
531
- "soc": [
532
- current_soc,
533
- current_soc,
534
- post_charge_soc,
535
- post_charge_soc,
536
- ],
537
- }
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
-
544
- # Create the charging event
545
- current_event = Event(
546
- scenario_id=scenario.id,
547
- vehicle_type_id=rotation.vehicle_type_id,
548
- vehicle=vehicle,
549
- station_id=trip.route.arrival_station_id,
550
- time_start=trip.arrival_time,
551
- time_end=next_trip.departure_time,
552
- soc_start=current_soc,
553
- soc_end=post_charge_soc,
554
- event_type=EventType.CHARGING_OPPORTUNITY,
555
- description=f"Opportunity charging event for trip {trip.id}.",
556
- timeseries=timeseries,
557
- )
558
- current_soc = post_charge_soc
559
- session.add(current_event)
418
+ current_soc = attempt_opportunity_charging_event(
419
+ previous_trip=trip,
420
+ next_trip=next_trip,
421
+ vehicle=vehicle,
422
+ charge_start_soc=current_soc,
423
+ terminus_deadtime=terminus_deadtime,
424
+ session=session,
425
+ )
560
426
 
561
427
 
562
428
  def generate_depot_layout(
@@ -0,0 +1,426 @@
1
+ import logging
2
+ from datetime import timedelta, datetime
3
+ from math import ceil
4
+ from typing import Tuple
5
+ from zoneinfo import ZoneInfo
6
+
7
+ import numpy as np
8
+ import sqlalchemy.orm
9
+ from eflips.model import (
10
+ Area,
11
+ Event,
12
+ EventType,
13
+ Rotation,
14
+ Vehicle,
15
+ Trip,
16
+ Station,
17
+ ChargeType,
18
+ )
19
+
20
+
21
+ def initialize_vehicle(rotation: Rotation, session: sqlalchemy.orm.session.Session):
22
+ """
23
+ Create and add a new Vehicle object in the database for the given rotation.
24
+
25
+ This function:
26
+ 1. Creates a new ``Vehicle`` instance using the provided rotation’s
27
+ vehicle type and scenario ID.
28
+ 2. Names it based on the rotation’s ID.
29
+ 3. Adds the vehicle to the specified SQLAlchemy session.
30
+ 4. Assigns the new vehicle to the rotation’s ``vehicle`` attribute.
31
+
32
+ :param rotation:
33
+ A :class:`Rotation` instance for which a new ``Vehicle`` should be created.
34
+ The new vehicle will inherit its type and scenario from this rotation.
35
+
36
+ :param session:
37
+ An active SQLAlchemy :class:`Session` used to persist the new vehicle to
38
+ the database. The vehicle is added to the session but not committed here.
39
+
40
+ :return:
41
+ ``None``. Changes are made to the session but are not committed yet.
42
+ """
43
+ vehicle = Vehicle(
44
+ vehicle_type_id=rotation.vehicle_type_id,
45
+ scenario_id=rotation.scenario_id,
46
+ name=f"Vehicle for rotation {rotation.id}",
47
+ )
48
+ session.add(vehicle)
49
+ rotation.vehicle = vehicle
50
+
51
+
52
+ def add_initial_standby_event(
53
+ vehicle: Vehicle, session: sqlalchemy.orm.session.Session
54
+ ):
55
+ """
56
+ Create and add a standby event immediately before the earliest trip of the given vehicle.
57
+
58
+ This function:
59
+ 1. Gathers all rotations assigned to the vehicle, sorted by their first trip’s departure time.
60
+ 2. Identifies the earliest trip across those rotations.
61
+ 3. Fetches an appropriate :class:`Area` record from the database based on
62
+ the vehicle's scenario and vehicle type (for depot and subloc capacity).
63
+ 4. Constructs a dummy standby event starting one second before the earliest trip’s
64
+ departure time, ending at the trip’s departure time, with 100% SoC.
65
+ 5. Adds the event to the session without committing (the caller is responsible for commits).
66
+
67
+ :param vehicle:
68
+ A :class:`Vehicle` instance for which to add a new standby event.
69
+ Must have associated rotations and trips.
70
+
71
+ :param session:
72
+ An active SQLAlchemy :class:`Session` used to persist the new event to
73
+ the database. The event is added to the session but not committed here.
74
+
75
+ :return:
76
+ ``None``. A new event is added to the session for the earliest trip,
77
+ but changes are not yet committed.
78
+ """
79
+ logger = logging.getLogger(__name__)
80
+
81
+ rotation_per_vehicle = sorted(
82
+ vehicle.rotations, key=lambda r: r.trips[0].departure_time
83
+ )
84
+
85
+ # Only keep the rotations that contain trips
86
+ rotation_per_vehicle = [r for r in rotation_per_vehicle if len(r.trips) > 0]
87
+ if len(rotation_per_vehicle) == 0:
88
+ logger.warning(f"No trips found for vehicle {vehicle.id}.")
89
+ return
90
+
91
+ earliest_trip = rotation_per_vehicle[0].trips[0]
92
+ area = (
93
+ session.query(Area)
94
+ .filter(Area.scenario_id == vehicle.scenario_id)
95
+ .filter(Area.vehicle_type_id == vehicle.vehicle_type_id)
96
+ .first()
97
+ )
98
+
99
+ standby_start = earliest_trip.departure_time - timedelta(seconds=1)
100
+ standby_event = Event(
101
+ scenario_id=vehicle.scenario_id,
102
+ vehicle_type_id=vehicle.vehicle_type_id,
103
+ vehicle=vehicle,
104
+ station_id=earliest_trip.route.departure_station_id,
105
+ subloc_no=0,
106
+ time_start=standby_start,
107
+ time_end=earliest_trip.departure_time,
108
+ soc_start=1,
109
+ soc_end=1,
110
+ event_type=EventType.STANDBY_DEPARTURE,
111
+ description=f"DUMMY Initial standby event for vehicle {vehicle.id}",
112
+ timeseries=None,
113
+ )
114
+ session.add(standby_event)
115
+
116
+
117
+ def find_charger_occupancy(
118
+ station: Station,
119
+ time_start: datetime,
120
+ time_end: datetime,
121
+ session: sqlalchemy.orm.session.Session,
122
+ resolution=timedelta(seconds=1),
123
+ ) -> Tuple[np.ndarray, np.ndarray]:
124
+ """
125
+ Build a timeseries of charger occupancy at a station between two points in time.
126
+
127
+ For each discrete timestep between ``time_start`` and ``time_end`` (at the given
128
+ ``resolution``), this function calculates how many charging events (from the database)
129
+ overlap with that time, thus producing a count of the active chargers at each timestep.
130
+
131
+ :param station:
132
+ The :class:`Station` whose charger occupancy is to be analyzed.
133
+ :param time_start:
134
+ The start time for the occupancy timeseries (inclusive).
135
+ :param time_end:
136
+ The end time for the occupancy timeseries (exclusive).
137
+ :param session:
138
+ An active SQLAlchemy :class:`Session` used to query the database.
139
+ :param resolution:
140
+ The timestep interval used to build the timeseries (default is 1 second).
141
+ Note that using a very fine resolution over a large time range can
142
+ produce large arrays.
143
+
144
+ :returns:
145
+ A tuple of two numpy arrays:
146
+ 1. ``times``: The array of discrete timesteps (shape: ``(n,)``).
147
+ 2. ``occupancy``: The array of integer occupancy values for each timestep
148
+ (shape: ``(n,)``), indicating how many charging events are active.
149
+ """
150
+ # Load all charging events that could be relevant
151
+ charging_events = (
152
+ session.query(Event)
153
+ .filter(
154
+ Event.event_type == EventType.CHARGING_OPPORTUNITY,
155
+ Event.station_id == station.id,
156
+ Event.time_start < time_end,
157
+ Event.time_end > time_start,
158
+ )
159
+ .all()
160
+ )
161
+
162
+ # We need to change the times to numpy datetime64 with implicit UTC timezone
163
+ tz = ZoneInfo("UTC")
164
+ time_start = np.datetime64(time_start.astimezone(tz).replace(tzinfo=None))
165
+ time_end = np.datetime64(time_end.astimezone(tz).replace(tzinfo=None))
166
+
167
+ times = np.arange(time_start, time_end, resolution)
168
+ occupancy = np.zeros_like(times, dtype=int)
169
+ for event in charging_events:
170
+ event_start = np.datetime64(
171
+ event.time_start.astimezone(tz).replace(tzinfo=None)
172
+ )
173
+ event_end = np.datetime64(event.time_end.astimezone(tz).replace(tzinfo=None))
174
+ start_idx = np.argmax(times >= event_start)
175
+ end_idx = np.argmax(times >= event_end)
176
+ occupancy[start_idx:end_idx] += 1
177
+
178
+ return times, occupancy
179
+
180
+
181
+ def find_best_timeslot(
182
+ station: Station,
183
+ time_start: datetime,
184
+ time_end: datetime,
185
+ charging_duration: timedelta,
186
+ session: sqlalchemy.orm.session.Session,
187
+ resolution: timedelta = timedelta(seconds=1),
188
+ ) -> datetime:
189
+ times, occupancy = find_charger_occupancy(
190
+ station, time_start, time_end, session, resolution=resolution
191
+ )
192
+
193
+ total_span = times[-1] - times[0]
194
+ if charging_duration > total_span:
195
+ raise ValueError("The event duration exceeds the entire timeseries span.")
196
+
197
+ ## AUTHOR: ChatGPT o-1
198
+ # Step 1: Compute how many indices are needed to cover `event_duration`.
199
+ steps_needed = int(charging_duration / resolution)
200
+ if steps_needed == 0:
201
+ raise ValueError("event_duration is too small for the timeseries resolution.")
202
+
203
+ # Step 2: Build a prefix-sum array for occupancy
204
+ prefix_sum = np.zeros(len(occupancy) + 1, dtype=float)
205
+ for i in range(len(occupancy)):
206
+ prefix_sum[i + 1] = prefix_sum[i] + occupancy[i]
207
+
208
+ # Step 3: Slide over every possible start index, compute sum in O(1)
209
+ best_start_idx = 0
210
+ min_sum = float("inf")
211
+ max_start_idx = len(occupancy) - steps_needed
212
+ if max_start_idx < 0:
213
+ raise ValueError("event_duration is too large for the timeseries resolution.")
214
+
215
+ for start_idx in range(max_start_idx + 1):
216
+ window_sum = prefix_sum[start_idx + steps_needed] - prefix_sum[start_idx]
217
+ if window_sum < min_sum:
218
+ min_sum = window_sum
219
+ best_start_idx = start_idx
220
+
221
+ best_start_time = times[best_start_idx]
222
+ # Turn it back into a datetime object with explicit UTC timezone
223
+ tz = ZoneInfo("UTC")
224
+ best_start_time = best_start_time.astype(datetime).replace(tzinfo=tz)
225
+
226
+ # Unused plot code to visually verify that it's working
227
+ if False:
228
+ # Convert numpy datetime array to matplotlib format
229
+ # If `times` is not numpy datetime64, you can skip this or adapt as needed.
230
+ # If `times` is a list of Python `datetime` objects, also skip the conversion step.
231
+ import matplotlib.pyplot as plt
232
+ import matplotlib.dates as mdates
233
+
234
+ fig, ax = plt.subplots(figsize=(10, 6))
235
+
236
+ # Plot the occupancy as a step or line plot
237
+ ax.plot(times, occupancy, label="Occupancy", drawstyle="steps-post", color="C0")
238
+
239
+ # Create a shaded region representing the best interval for the event
240
+ event_start = best_start_time
241
+ event_end = best_start_time + charging_duration
242
+ ax.axvspan(
243
+ event_start, event_end, color="C2", alpha=0.3, label="Chosen Interval"
244
+ )
245
+
246
+ # Format the x-axis to show date/time
247
+ # This only applies if your `times` are datetime objects or convertible to them
248
+ ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%d %H:%M:%S"))
249
+ plt.xticks(rotation=45, ha="right")
250
+
251
+ ax.set_xlabel("Time")
252
+ ax.set_ylabel("Occupancy (# of events)")
253
+ ax.set_title("Charger Occupancy with Chosen Event Interval")
254
+ ax.legend()
255
+ ax.grid(True)
256
+
257
+ plt.tight_layout()
258
+ plt.show()
259
+
260
+ return best_start_time
261
+
262
+
263
+ def attempt_opportunity_charging_event(
264
+ previous_trip: Trip,
265
+ next_trip: Trip,
266
+ vehicle: Vehicle,
267
+ charge_start_soc: float,
268
+ terminus_deadtime: timedelta,
269
+ session: sqlalchemy.orm.session.Session,
270
+ ) -> float:
271
+ logger = logging.getLogger(__name__)
272
+
273
+ # Sanity checks
274
+ if previous_trip.route.arrival_station_id != next_trip.route.departure_station_id:
275
+ raise ValueError(
276
+ f"Trips {previous_trip.id} and {next_trip.id} are not consecutive."
277
+ )
278
+ if previous_trip.rotation_id != next_trip.rotation_id:
279
+ raise ValueError(
280
+ f"Trips {previous_trip.id} and {next_trip.id} are not in the same rotation."
281
+ )
282
+ if not (previous_trip.scenario_id == next_trip.scenario_id == vehicle.scenario_id):
283
+ raise ValueError(
284
+ f"Trips {previous_trip.id} and {next_trip.id} are not in the same scenario."
285
+ )
286
+ if not (
287
+ vehicle.vehicle_type.opportunity_charging_capable
288
+ and next_trip.rotation.allow_opportunity_charging
289
+ and previous_trip.route.arrival_station.is_electrified
290
+ and previous_trip.route.arrival_station.charge_type == ChargeType.OPPORTUNITY
291
+ ):
292
+ raise ValueError(
293
+ "Opportunity charging was requested even though it is not possible."
294
+ )
295
+
296
+ # Identify the break time between trips
297
+ break_time = next_trip.departure_time - previous_trip.arrival_time
298
+
299
+ if break_time > terminus_deadtime:
300
+ logger.debug(f"Adding opportunity charging event after trip {previous_trip.id}")
301
+
302
+ # How much energy can be charged in this time?
303
+ max_recharged_energy = (
304
+ max([v[1] for v in vehicle.vehicle_type.charging_curve])
305
+ * (break_time.total_seconds() - terminus_deadtime.total_seconds())
306
+ / 3600
307
+ )
308
+ needed_energy = (1 - charge_start_soc) * vehicle.vehicle_type.battery_capacity
309
+
310
+ if max_recharged_energy < needed_energy:
311
+ # We do not need to shift the time around. Just charge as much as possible
312
+ time_event_start = previous_trip.arrival_time
313
+ time_charge_start = time_event_start + terminus_deadtime / 2
314
+ time_charge_end = next_trip.departure_time - terminus_deadtime / 2
315
+ time_event_end = next_trip.departure_time
316
+
317
+ soc_event_start = charge_start_soc
318
+ soc_charge_start = charge_start_soc
319
+ soc_charge_end = (
320
+ charge_start_soc
321
+ + max_recharged_energy / vehicle.vehicle_type.battery_capacity
322
+ )
323
+ assert soc_charge_end <= 1
324
+ soc_event_end = soc_charge_end
325
+ else:
326
+ needed_duration_purely_charing = timedelta(
327
+ seconds=(
328
+ ceil(
329
+ needed_energy
330
+ * 3600
331
+ / max([v[1] for v in vehicle.vehicle_type.charging_curve])
332
+ )
333
+ )
334
+ )
335
+ needed_duration_total = needed_duration_purely_charing + terminus_deadtime
336
+
337
+ # We have to shift the time around to the time with the lowest occupancy
338
+ # Within this time band.
339
+
340
+ best_start_time = find_best_timeslot(
341
+ previous_trip.route.arrival_station,
342
+ previous_trip.arrival_time,
343
+ next_trip.departure_time,
344
+ needed_duration_total,
345
+ session,
346
+ )
347
+ time_event_start = best_start_time
348
+ time_charge_start = best_start_time + terminus_deadtime / 2
349
+ time_charge_end = time_charge_start + needed_duration_purely_charing
350
+ time_event_end = time_charge_end + (terminus_deadtime / 2)
351
+
352
+ soc_event_start = charge_start_soc
353
+ soc_charge_start = charge_start_soc
354
+ soc_charge_end = 1
355
+ soc_event_end = 1
356
+
357
+ # Create a simple timeseries for the charging event
358
+ timeseries = {
359
+ "time": [
360
+ time_event_start.isoformat(),
361
+ time_charge_start.isoformat(),
362
+ time_charge_end.isoformat(),
363
+ time_event_end.isoformat(),
364
+ ],
365
+ "soc": [soc_event_start, soc_charge_start, soc_charge_end, soc_event_end],
366
+ }
367
+
368
+ # Create the charging event
369
+ current_event = Event(
370
+ scenario_id=vehicle.scenario_id,
371
+ vehicle_type_id=vehicle.vehicle_type_id,
372
+ vehicle=vehicle,
373
+ station_id=previous_trip.route.arrival_station_id,
374
+ time_start=time_event_start,
375
+ time_end=time_event_end,
376
+ soc_start=charge_start_soc,
377
+ soc_end=soc_event_end,
378
+ event_type=EventType.CHARGING_OPPORTUNITY,
379
+ description=f"Opportunity charging event after trip {previous_trip.id}.",
380
+ timeseries=timeseries,
381
+ )
382
+ session.add(current_event)
383
+
384
+ # If there is time between the previous trip's end and the charging event's start, add a STANDBY event
385
+ if time_event_start > previous_trip.arrival_time:
386
+ standby_event = Event(
387
+ scenario_id=vehicle.scenario_id,
388
+ vehicle_type_id=vehicle.vehicle_type_id,
389
+ vehicle=vehicle,
390
+ station_id=previous_trip.route.arrival_station_id,
391
+ time_start=previous_trip.arrival_time,
392
+ time_end=time_event_start,
393
+ soc_start=charge_start_soc, # SoC is unchanged while in STANDBY
394
+ soc_end=charge_start_soc,
395
+ event_type=EventType.STANDBY,
396
+ description=f"Standby event before charging after trip {previous_trip.id}.",
397
+ timeseries=None,
398
+ )
399
+ session.add(standby_event)
400
+
401
+ # If there is time between the charging event's end and the next trip's start, add a STANDBY_DEPARTURE event
402
+ if time_event_end < next_trip.departure_time:
403
+ standby_departure_event = Event(
404
+ scenario_id=vehicle.scenario_id,
405
+ vehicle_type_id=vehicle.vehicle_type_id,
406
+ vehicle=vehicle,
407
+ station_id=previous_trip.route.arrival_station_id,
408
+ time_start=time_event_end,
409
+ time_end=next_trip.departure_time,
410
+ soc_start=soc_event_end, # SoC is unchanged while in STANDBY
411
+ soc_end=soc_event_end,
412
+ event_type=EventType.STANDBY_DEPARTURE,
413
+ description=(
414
+ f"Standby departure event after charging, before trip {next_trip.id}."
415
+ ),
416
+ timeseries=None,
417
+ )
418
+ session.add(standby_departure_event)
419
+
420
+ return soc_event_end
421
+
422
+ else:
423
+ logger.debug(
424
+ f"No opportunity charging event added after trip {previous_trip.id}"
425
+ )
426
+ return charge_start_soc
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: eflips-depot
3
- Version: 4.5.0
3
+ Version: 4.6.1
4
4
  Summary: Depot Simulation for eFLIPS
5
5
  License: AGPL-3.0-or-later
6
6
  Author: Enrico Lauth
@@ -1,7 +1,8 @@
1
1
  eflips/depot/__init__.py,sha256=RQ_UKNrGWA6q17TZFu86ai8pC7qCpcbmAgVKh7aImwo,1613
2
- eflips/depot/api/__init__.py,sha256=g5TsPqkP_u0HTDcerRV8IQ0kZloFEJB8GVsnxbaLcUA,54896
2
+ eflips/depot/api/__init__.py,sha256=Sxsqj5qIcSK-dqtE2UqaHVWEWnycU65N-pDnLuTJfeU,48244
3
3
  eflips/depot/api/defaults/default_settings.json,sha256=0eUDTw_rtLQFvthP8oJL93iRXlmAOravAg-4qqGMQAY,5375
4
4
  eflips/depot/api/private/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ eflips/depot/api/private/consumption.py,sha256=WIsXTPSC-GEnzzAeojHv80ylosdKghYjZp0-HAKIsto,16593
5
6
  eflips/depot/api/private/depot.py,sha256=v5Edb0sQP2QNIyBLvccVzAK9_kxCszar0cOu5ciFyrw,40780
6
7
  eflips/depot/api/private/results_to_database.py,sha256=Sh2VJ3k60QJ5RGkc8sw-7XbljiMe65EHeoagKIpYlHM,24585
7
8
  eflips/depot/api/private/smart_charging.py,sha256=MQ9fXdKByHAz6RSKXYcpJXDBccdJKZ2qGReCHagVCyo,13033
@@ -36,7 +37,7 @@ eflips/depot/simulation.py,sha256=ee0qTzOzG-8ybN36ie_NJallXfC7jUaS9JZvaYFziLs,10
36
37
  eflips/depot/smart_charging.py,sha256=C3BYqzn2-OYY4ipXm0ETtavbAM9QXZMYULBpVoChf0E,54311
37
38
  eflips/depot/standalone.py,sha256=VxcTzBaB67fNJUMmjPRwKXjhqTy6oQ41Coote2LvAmk,22338
38
39
  eflips/depot/validation.py,sha256=TIuY7cQtEJI4H2VVMSuY5IIVkacEEZ67weeMuY3NSAM,7097
39
- eflips_depot-4.5.0.dist-info/LICENSE.md,sha256=KB4XTk1fPHjtZCYDyPyreu6h1LVJVZXYg-5vePcWZAc,34143
40
- eflips_depot-4.5.0.dist-info/METADATA,sha256=x0pPpcq2UaNNSNsue_tNcMStgHc055VymCV4QlaFATk,5940
41
- eflips_depot-4.5.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
42
- eflips_depot-4.5.0.dist-info/RECORD,,
40
+ eflips_depot-4.6.1.dist-info/LICENSE.md,sha256=KB4XTk1fPHjtZCYDyPyreu6h1LVJVZXYg-5vePcWZAc,34143
41
+ eflips_depot-4.6.1.dist-info/METADATA,sha256=NE2AQv4V2D92MnSOVSNP4doVRPO1aCSYVmqezpQnggM,5940
42
+ eflips_depot-4.6.1.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
43
+ eflips_depot-4.6.1.dist-info/RECORD,,