eflips-depot 4.3.17__py3-none-any.whl → 4.3.18__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.
@@ -26,7 +26,6 @@ The following steps are recommended for using the API:
26
26
  """
27
27
  import copy
28
28
  import datetime
29
- import itertools
30
29
  import logging
31
30
  import os
32
31
  import warnings
@@ -37,16 +36,13 @@ from enum import Enum
37
36
  from math import ceil
38
37
  from typing import Any, Dict, Optional, Union, List
39
38
 
40
- import numpy as np
41
39
  import sqlalchemy.orm
42
40
  from eflips.model import (
43
41
  Area,
44
42
  AreaType,
45
- AssocPlanProcess,
46
43
  Depot,
47
44
  Event,
48
45
  EventType,
49
- Plan,
50
46
  Rotation,
51
47
  Scenario,
52
48
  Station,
@@ -54,18 +50,12 @@ from eflips.model import (
54
50
  Vehicle,
55
51
  VehicleType,
56
52
  )
57
- from eflips.model import Process
58
53
  from sqlalchemy.orm import Session
59
- from sqlalchemy.sql import select
60
54
 
61
55
  import eflips.depot
62
56
  from eflips.depot import (
63
57
  DepotEvaluation,
64
- DirectArea,
65
- LineArea,
66
- ProcessStatus,
67
58
  SimulationHost,
68
- SimpleVehicle,
69
59
  )
70
60
  from eflips.depot.api.private.depot import (
71
61
  _generate_all_direct_depot,
@@ -73,6 +63,18 @@ from eflips.depot.api.private.depot import (
73
63
  delete_depots,
74
64
  depot_to_template,
75
65
  group_rotations_by_start_end_stop,
66
+ generate_line_depot_layout,
67
+ real_peak_area_utilization,
68
+ real_peak_vehicle_count,
69
+ )
70
+ from eflips.depot.api.private.results_to_database import (
71
+ get_finished_schedules_per_vehicle,
72
+ generate_vehicle_events,
73
+ complete_standby_departure_events,
74
+ add_soc_to_events,
75
+ add_events_into_database,
76
+ update_vehicle_in_rotation,
77
+ update_waiting_events,
76
78
  )
77
79
  from eflips.depot.api.private.smart_charging import optimize_charging_events_even
78
80
  from eflips.depot.api.private.util import (
@@ -83,7 +85,6 @@ from eflips.depot.api.private.util import (
83
85
  VehicleSchedule,
84
86
  check_depot_validity,
85
87
  )
86
- from eflips.depot.evaluation import to_prev_values
87
88
 
88
89
 
89
90
  class SmartChargingStrategy(Enum):
@@ -386,231 +387,6 @@ def simple_consumption_simulation(
386
387
  session.add(current_event)
387
388
 
388
389
 
389
- def generate_line_depot_layout(
390
- CLEAN_DURATION: int,
391
- charging_power: float,
392
- station: Station,
393
- scenario: Scenario,
394
- session: sqlalchemy.orm.session.Session,
395
- direct_counts: Dict[VehicleType, int],
396
- line_counts: Dict[VehicleType, int],
397
- line_length: int,
398
- vehicle_type_rotation_dict: Dict[VehicleType, List[Rotation]],
399
- shunting_duration: timedelta = timedelta(minutes=5),
400
- ) -> None:
401
- """
402
- Generate a depot layout with line areas and direct areas.
403
-
404
- :param CLEAN_DURATION: The duration of the cleaning process in seconds.
405
- :param charging_power: The charging power of the charging area in kW.
406
- :param station: The stop where the depot is located.
407
- :param scenario: The scenario for which the depot layout should be generated.
408
- :param session: The SQLAlchemy session object.
409
- :param direct_counts: A dictionary with vehicle types as keys and the number of vehicles in the direct areas as
410
- values.
411
- :param line_counts: A dictionary with vehicle types as keys and the number of vehicles in the line areas as values.
412
- :param line_length: The length of the line areas.
413
- :param vehicle_type_rotation_dict: A dictionary with vehicle types as keys and rotations as values.
414
- :return: The number of cleaning areas and the number of shunting areas.
415
- """
416
- logger = logging.getLogger(__name__)
417
- DEBUG_PLOT = False
418
-
419
- # In order to figure out how many cleaning areas we need, we look at the number of vehicle simultaneously being
420
- # cleaned. This is the number of vehicles simulatenously being within the "CLEAN_DURATION" after their arrival.
421
-
422
- # We assemble a vector of all time in the simulation
423
- logger.info("Calculating the number of cleaning areas needed")
424
- all_rotations = list(itertools.chain(*vehicle_type_rotation_dict.values()))
425
- start_time = min(
426
- [rotation.trips[0].departure_time for rotation in all_rotations]
427
- ).timestamp()
428
- end_time = max(
429
- [rotation.trips[-1].arrival_time for rotation in all_rotations]
430
- ).timestamp()
431
- timestamps_to_sample = np.arange(start_time, end_time, 60)
432
- clean_occupancy = np.zeros_like(timestamps_to_sample)
433
-
434
- # Then fir each arrival, we add 1 to the CLEAN_DURATION after the arrival
435
- for rotation in all_rotations:
436
- rotation_end = rotation.trips[-1].arrival_time.timestamp()
437
- clean_occupancy += np.interp(
438
- timestamps_to_sample,
439
- [rotation_end, rotation_end + CLEAN_DURATION],
440
- [1, 1],
441
- left=0,
442
- right=0,
443
- )
444
-
445
- if DEBUG_PLOT:
446
- from matplotlib import pyplot as plt
447
-
448
- plt.figure()
449
- plt.plot(timestamps_to_sample, clean_occupancy)
450
- plt.show()
451
-
452
- vehicles_arriving_in_window = int(max(clean_occupancy))
453
- logger.info(
454
- f"Number of vehicles arriving in a {CLEAN_DURATION/60:.1f} minute window: {vehicles_arriving_in_window:.0f}"
455
- )
456
-
457
- # Take a fifth of the vehicles arriving in the window as the number of cleaning areas needed
458
- clean_areas_needed = ceil(vehicles_arriving_in_window / 2)
459
- logger.info(f"Number of cleaning areas created: {clean_areas_needed}")
460
- del all_rotations, clean_occupancy, timestamps_to_sample, start_time, end_time
461
-
462
- # Create the depot
463
- # `vehicles_arriving_in_window`+1 will be the size of our shunting areas
464
- # `clean_areas_needed` will be the size of our cleaning areas
465
- # We will create line and direct areas for each vehicle type
466
- # - THe line areas will be of length `line_length` and count `line_counts[vehicle_type]`
467
- # - The direct areas will be of length 1 and count `direct_counts[vehicle_type]`
468
- # - The charging power for the line areas will be `charging_power_direct` and for the direct areas `charging_power`
469
- # unless `charging_power_direct` is not set, in which case `charging_power` will be used.
470
-
471
- # Create the depot
472
- depot = Depot(
473
- scenario=scenario,
474
- name=f"Depot at {station.name}",
475
- name_short=station.name_short,
476
- station_id=station.id,
477
- )
478
- session.add(depot)
479
-
480
- shunting_1 = Process(
481
- name="Shunting 1 (Arrival -> Cleaning)",
482
- scenario=scenario,
483
- dispatchable=False,
484
- duration=shunting_duration,
485
- )
486
-
487
- session.add(shunting_1)
488
- clean = Process(
489
- name="Arrival Cleaning",
490
- scenario=scenario,
491
- dispatchable=False,
492
- duration=timedelta(seconds=CLEAN_DURATION),
493
- )
494
- session.add(clean)
495
-
496
- shunting_2 = Process(
497
- name="Shunting 2 (Cleaning -> Charging)",
498
- scenario=scenario,
499
- dispatchable=False,
500
- duration=shunting_duration,
501
- )
502
- session.add(shunting_2)
503
-
504
- charging = Process(
505
- name="Charging",
506
- scenario=scenario,
507
- dispatchable=True,
508
- electric_power=charging_power,
509
- )
510
-
511
- standby_departure = Process(
512
- name="Standby Pre-departure",
513
- scenario=scenario,
514
- dispatchable=True,
515
- )
516
- session.add(standby_departure)
517
-
518
- # Create shared waiting area
519
- # This will be the "virtual" area where vehicles wait for a spot in the depot
520
- waiting_area = Area(
521
- scenario=scenario,
522
- name=f"Waiting Area for every type of vehicle",
523
- depot=depot,
524
- area_type=AreaType.DIRECT_ONESIDE,
525
- capacity=100,
526
- )
527
- session.add(waiting_area)
528
-
529
- # Create a shared shunting area (large enough to fit all rotations)
530
- shunting_area_1 = Area(
531
- scenario=scenario,
532
- name=f"Shunting Area 1 (Arrival -> Cleaning)",
533
- depot=depot,
534
- area_type=AreaType.DIRECT_ONESIDE,
535
- capacity=sum(
536
- [len(rotations) for rotations in vehicle_type_rotation_dict.values()]
537
- ), # TODO
538
- )
539
- session.add(shunting_area_1)
540
- shunting_area_1.processes.append(shunting_1)
541
-
542
- # Create a shared cleaning area
543
- cleaning_area = Area(
544
- scenario=scenario,
545
- name=f"Cleaning Area",
546
- depot=depot,
547
- area_type=AreaType.DIRECT_ONESIDE,
548
- capacity=clean_areas_needed,
549
- )
550
- session.add(cleaning_area)
551
- cleaning_area.processes.append(clean)
552
-
553
- # Create a shared shunting area
554
- shunting_area_2 = Area(
555
- scenario=scenario,
556
- name=f"Shunting Area 2 (Cleaning -> Charging)",
557
- depot=depot,
558
- area_type=AreaType.DIRECT_ONESIDE,
559
- capacity=clean_areas_needed,
560
- )
561
- session.add(shunting_area_2)
562
- shunting_area_2.processes.append(shunting_2)
563
-
564
- # Create the line areas for each vehicle type
565
- for vehicle_type, count in line_counts.items():
566
- for i in range(count):
567
- line_area = Area(
568
- scenario=scenario,
569
- name=f"Line Area for {vehicle_type.name} #{i+1:02d}",
570
- depot=depot,
571
- area_type=AreaType.LINE,
572
- capacity=line_length,
573
- vehicle_type=vehicle_type,
574
- )
575
- session.add(line_area)
576
- line_area.processes.append(charging)
577
- line_area.processes.append(standby_departure)
578
-
579
- # Create the direct areas for each vehicle type
580
- for vehicle_type, count in direct_counts.items():
581
- if count > 0:
582
- direct_area = Area(
583
- scenario=scenario,
584
- name=f"Direct Area for {vehicle_type.name}",
585
- depot=depot,
586
- area_type=AreaType.DIRECT_ONESIDE,
587
- capacity=count,
588
- vehicle_type=vehicle_type,
589
- )
590
- session.add(direct_area)
591
- direct_area.processes.append(charging)
592
- direct_area.processes.append(standby_departure)
593
-
594
- # Create the plan
595
- # Create plan
596
- plan = Plan(scenario=scenario, name=f"Default Plan")
597
- session.add(plan)
598
-
599
- depot.default_plan = plan
600
-
601
- # Create the assocs in order to put the areas in the plan
602
- assocs = [
603
- AssocPlanProcess(scenario=scenario, process=shunting_1, plan=plan, ordinal=0),
604
- AssocPlanProcess(scenario=scenario, process=clean, plan=plan, ordinal=1),
605
- AssocPlanProcess(scenario=scenario, process=shunting_2, plan=plan, ordinal=2),
606
- AssocPlanProcess(scenario=scenario, process=charging, plan=plan, ordinal=3),
607
- AssocPlanProcess(
608
- scenario=scenario, process=standby_departure, plan=plan, ordinal=4
609
- ),
610
- ]
611
- session.add_all(assocs)
612
-
613
-
614
390
  @dataclass
615
391
  class DepotConfiguration:
616
392
  charging_power: float
@@ -621,136 +397,6 @@ class DepotConfiguration:
621
397
  num_shunting_areas: int
622
398
 
623
399
 
624
- def real_peak_area_utilization(ev: DepotEvaluation) -> Dict[str, Dict[AreaType, int]]:
625
- """
626
- Calculate the real peak vehicle count for a depot evaluation by vehicle type and area type.
627
-
628
- For the line areas, the maximum number of lines in use at the same time is calculated.
629
-
630
- :param ev: A DepotEvaluation object.
631
- :return: The real peak vehicle count by vehicle type and area type.
632
- """
633
- area_types_by_id: Dict[int, AreaType] = dict()
634
- total_counts_by_area: Dict[str, Dict[str, np.ndarray]] = dict()
635
-
636
- # We are assuming that the smulation runs for at least four days
637
- SECONDS_IN_A_DAY = 24 * 60 * 60
638
- assert ev.SIM_TIME >= 4 * SECONDS_IN_A_DAY
639
-
640
- for area in ev.depot.list_areas:
641
- # We need to figure out which kind of area this is
642
- # We do this by looking at the vehicle type of the area
643
- if len(area.entry_filter.filters) > 0:
644
- if isinstance(area, LineArea):
645
- area_types_by_id[area.ID] = AreaType.LINE
646
- elif isinstance(area, DirectArea):
647
- area_types_by_id[area.ID] = AreaType.DIRECT_ONESIDE
648
- else:
649
- raise ValueError("Unknown area type")
650
-
651
- assert len(area.entry_filter.vehicle_types_str) == 1
652
- vehicle_type_name = area.entry_filter.vehicle_types_str[0]
653
-
654
- nv = area.logger.get_valList("count", SIM_TIME=ev.SIM_TIME)
655
- nv = to_prev_values(nv)
656
- nv = np.array(nv)
657
-
658
- # If the area is empty, we don't care about it
659
- if np.all(nv == 0):
660
- continue
661
-
662
- if vehicle_type_name not in total_counts_by_area:
663
- total_counts_by_area[vehicle_type_name] = dict()
664
- # We don't want the last day, as all vehicles will re-enter the depot
665
- total_counts_by_area[vehicle_type_name][area.ID] = nv[:-SECONDS_IN_A_DAY]
666
- else:
667
- # This is an area for all vehicle types
668
- # We don't care about this
669
- continue
670
-
671
- if False:
672
- from matplotlib import pyplot as plt
673
-
674
- for vehicle_type_name, counts in total_counts_by_area.items():
675
- plt.figure()
676
- for area_id, proper_counts in counts.items():
677
- # dashed if direct, solid if line
678
- if area_types_by_id[area_id] == AreaType.DIRECT_ONESIDE:
679
- plt.plot(proper_counts, "--", label=area_id)
680
- else:
681
- plt.plot(proper_counts, label=area_id)
682
- plt.legend()
683
- plt.show()
684
-
685
- # Calculate the maximum utilization of the direct areas and the maximum number of lines in use at the same time
686
- # Per vehicle type
687
- ret_val: Dict[str, Dict[AreaType, int]] = dict()
688
- for vehicle_type_name, count_dicts in total_counts_by_area.items():
689
- peak_direct_area_usage = 0
690
- number_of_lines_in_use = 0
691
- for area_id, counts in count_dicts.items():
692
- if area_types_by_id[area_id] == AreaType.DIRECT_ONESIDE:
693
- peak_direct_area_usage += max(peak_direct_area_usage, np.max(counts))
694
- else:
695
- number_of_lines_in_use += 1
696
-
697
- ret_val[vehicle_type_name] = {
698
- AreaType.DIRECT_ONESIDE: int(peak_direct_area_usage),
699
- AreaType.LINE: int(number_of_lines_in_use),
700
- }
701
-
702
- return ret_val
703
-
704
-
705
- def real_peak_vehicle_count(ev: DepotEvaluation) -> Dict[str, int]:
706
- """
707
- Calculate the real peak vehicle count for a depot evaluation.
708
-
709
- This is different from the amount of vehicles used
710
- in the calculation, as towards the end of the simulation all vehicles will re-enter-the depot, which leads to
711
- a lower actual peak vehicle count than what `nvehicles_used_calculation` returns.
712
- :param ev: A DepotEvaluation object.
713
- :return: The real peak vehicle count. This is what the depot layout should be designed for.
714
- """
715
-
716
- total_counts_by_vehicle_type: Dict[str, np.ndarray] = dict()
717
-
718
- for area in ev.depot.list_areas:
719
- # We need to figure out which kind of area this is
720
- # We do this by looking at the vehicle type of the area
721
- if len(area.entry_filter.filters) > 0:
722
- assert len(area.entry_filter.vehicle_types_str) == 1
723
- vehicle_type_name = area.entry_filter.vehicle_types_str[0]
724
-
725
- nv = area.logger.get_valList("count", SIM_TIME=ev.SIM_TIME)
726
- nv = to_prev_values(nv)
727
- nv = np.array(nv)
728
-
729
- if vehicle_type_name not in total_counts_by_vehicle_type:
730
- total_counts_by_vehicle_type[vehicle_type_name] = np.zeros(
731
- ev.SIM_TIME, dtype=np.int32
732
- )
733
- total_counts_by_vehicle_type[vehicle_type_name] += nv
734
- else:
735
- # This is an area for all vehicle types
736
- # We don't care about this
737
- continue
738
-
739
- # We are assuming that the smulation runs for at least four days
740
- SECONDS_IN_A_DAY = 24 * 60 * 60
741
- assert ev.SIM_TIME >= 4 * SECONDS_IN_A_DAY
742
-
743
- # Towards the end, all the vehicles will re-enter the depot
744
- # So our practital peak vehicle count is the maximum excluding the last day
745
- for vehicle_type_name, counts in total_counts_by_vehicle_type.items():
746
- total_counts_by_vehicle_type[vehicle_type_name] = counts[:-SECONDS_IN_A_DAY]
747
-
748
- return {
749
- vehicle_type_name: int(np.max(counts))
750
- for vehicle_type_name, counts in total_counts_by_vehicle_type.items()
751
- }
752
-
753
-
754
400
  def generate_realistic_depot_layout(
755
401
  scenario: Union[Scenario, int, Any],
756
402
  charging_power: float,
@@ -1236,18 +882,6 @@ def simulate_scenario(
1236
882
  raise NotImplementedError()
1237
883
 
1238
884
 
1239
- def _init_simulation(
1240
- scenario: Scenario,
1241
- session: Session,
1242
- repetition_period: Optional[timedelta] = None,
1243
- vehicle_count_dict: Optional[Dict[str, int]] = None,
1244
- ) -> SimulationHost:
1245
- """Deprecated stub for init_simulation."""
1246
- raise NotImplementedError(
1247
- "The function _init_simulation is deprecated. Please use init_simulation instead."
1248
- )
1249
-
1250
-
1251
885
  def init_simulation(
1252
886
  scenario: Scenario,
1253
887
  session: Session,
@@ -1434,13 +1068,6 @@ def init_simulation(
1434
1068
  return simulation_host
1435
1069
 
1436
1070
 
1437
- def _run_simulation(simulation_host: SimulationHost) -> DepotEvaluation:
1438
- """Deprecated stub for run_simulation."""
1439
- raise NotImplementedError(
1440
- "The function _run_simulation is deprecated. Please use run_simulation instead."
1441
- )
1442
-
1443
-
1444
1071
  def run_simulation(simulation_host: SimulationHost) -> Dict[str, DepotEvaluation]:
1445
1072
  """Run simulation and return simulation results.
1446
1073
 
@@ -1488,17 +1115,6 @@ def run_simulation(simulation_host: SimulationHost) -> Dict[str, DepotEvaluation
1488
1115
  return results
1489
1116
 
1490
1117
 
1491
- def _add_evaluation_to_database(
1492
- scenario_id: int,
1493
- depot_evaluation: DepotEvaluation,
1494
- session: sqlalchemy.orm.Session,
1495
- ) -> None:
1496
- """Deprecated stub for add_evaluation_to_database."""
1497
- raise NotImplementedError(
1498
- "The function _add_evaluation_to_database is deprecated. Please use add_evaluation_to_database instead."
1499
- )
1500
-
1501
-
1502
1118
  def insert_dummy_standby_departure_events(
1503
1119
  depot_id: int, session: Session, sim_time_end: Optional[datetime.datetime] = None
1504
1120
  ) -> None:
@@ -1640,7 +1256,7 @@ def add_evaluation_to_database(
1640
1256
  # Earliest and latest time defines a time window, only the events within this time window will be
1641
1257
  # handled. It is usually the departure time of the last copy trip in the "early-shifted" copy
1642
1258
  # schedules and the departure time of the first copy trip in the "late-shifted" copy schedules.
1643
- ) = _get_finished_schedules_per_vehicle(
1259
+ ) = get_finished_schedules_per_vehicle(
1644
1260
  dict_of_events, current_vehicle.finished_trips, current_vehicle_db.id
1645
1261
  )
1646
1262
 
@@ -1660,7 +1276,7 @@ def add_evaluation_to_database(
1660
1276
 
1661
1277
  list_of_assigned_schedules.extend(schedule_current_vehicle)
1662
1278
 
1663
- _generate_vehicle_events(
1279
+ generate_vehicle_events(
1664
1280
  dict_of_events,
1665
1281
  current_vehicle,
1666
1282
  waiting_area_id,
@@ -1670,9 +1286,9 @@ def add_evaluation_to_database(
1670
1286
 
1671
1287
  # Python passes dictionaries by reference
1672
1288
 
1673
- _complete_standby_departure_events(dict_of_events, latest_time)
1289
+ complete_standby_departure_events(dict_of_events, latest_time)
1674
1290
 
1675
- _add_soc_to_events(dict_of_events, current_vehicle.battery_logs)
1291
+ add_soc_to_events(dict_of_events, current_vehicle.battery_logs)
1676
1292
 
1677
1293
  try:
1678
1294
  assert (not dict_of_events) is False
@@ -1684,7 +1300,7 @@ def add_evaluation_to_database(
1684
1300
 
1685
1301
  continue
1686
1302
 
1687
- _add_events_into_database(
1303
+ add_events_into_database(
1688
1304
  current_vehicle_db,
1689
1305
  dict_of_events,
1690
1306
  session,
@@ -1693,581 +1309,5 @@ def add_evaluation_to_database(
1693
1309
  )
1694
1310
 
1695
1311
  # Postprocessing of events
1696
- _update_vehicle_in_rotation(session, scenario, list_of_assigned_schedules)
1697
- _update_waiting_events(session, scenario, waiting_area_id)
1698
-
1699
-
1700
- def _get_finished_schedules_per_vehicle(
1701
- dict_of_events, list_of_finished_trips: List, db_vehicle_id: int
1702
- ):
1703
- """
1704
- This function completes the following tasks:
1705
-
1706
- 1. It gets the finished non-copy schedules of the current vehicle,
1707
- which will be used in :func:`_update_vehicle_in_rotation()`.
1708
-
1709
- 2. It fills the dictionary of events with the trip_ids of the current vehicle.
1710
-
1711
- 3. It returns an earliest and a latest time according to this vehicle's schedules. Only processes happening within
1712
- this time window will be handled later.
1713
-
1714
- Usually the earliest time is the departure time of the last copy trip in the "early-shifted" copy schedules
1715
- and the lastest time is the departure time of the first copy trip in the "late-shifted" copy schedules.
1716
-
1717
- # If the vehicle's first trip is a non-copy trip, the earliest time is the departure time of the first trip. If the
1718
- # vehicle's last trip is a non-copy trip, the latest time is the departure time of the last trip.
1719
-
1720
- :param dict_of_events: An ordered dictionary storing the data related to an event. The keys are the start times of
1721
- the events.
1722
- :param list_of_finished_trips: A list of finished trips of a vehicle directly from
1723
- :class:`eflips.depot.simple_vehicle.SimpleVehicle` object.
1724
-
1725
- :param db_vehicle_id: The vehicle id in the database.
1726
-
1727
- :return: A tuple of three elements. The first element is a list of finished schedules of the vehicle. The second and
1728
- third elements are the earliest and latest time of the vehicle's schedules.
1729
- """
1730
- finished_schedules = []
1731
-
1732
- list_of_finished_trips.sort(key=lambda x: x.atd)
1733
- earliest_time = None
1734
- latest_time = None
1735
-
1736
- for i in range(len(list_of_finished_trips)):
1737
- assert list_of_finished_trips[i].atd == list_of_finished_trips[i].std, (
1738
- "The trip {current_trip.ID} is delayed. The simulation doesn't "
1739
- "support delayed trips for now."
1740
- )
1741
-
1742
- if list_of_finished_trips[i].is_copy is False:
1743
- current_trip = list_of_finished_trips[i]
1744
-
1745
- finished_schedules.append((int(current_trip.ID), db_vehicle_id))
1746
- dict_of_events[current_trip.atd] = {
1747
- "type": "Trip",
1748
- "id": int(current_trip.ID),
1749
- }
1750
- if i == 0:
1751
- raise ValueError(
1752
- f"New Vehicle required for the trip {current_trip.ID}, which suggests the fleet or the "
1753
- f"infrastructure might not be enough for the full electrification. Please add charging "
1754
- f"interfaces or increase charging power ."
1755
- )
1756
-
1757
- elif i != 0 and i == len(list_of_finished_trips) - 1:
1758
- # Vehicle's last trip is a non-copy trip
1759
- if earliest_time is None:
1760
- earliest_time = list_of_finished_trips[i - 1].ata
1761
- latest_time = list_of_finished_trips[i].ata
1762
-
1763
- else:
1764
- if list_of_finished_trips[i - 1].is_copy is True:
1765
- earliest_time = list_of_finished_trips[i - 1].ata
1766
- if list_of_finished_trips[i + 1].is_copy is True:
1767
- latest_time = list_of_finished_trips[i + 1].atd
1768
-
1769
- return finished_schedules, earliest_time, latest_time
1770
-
1771
-
1772
- def _generate_vehicle_events(
1773
- dict_of_events,
1774
- current_vehicle: SimpleVehicle,
1775
- virtual_waiting_area_id: int,
1776
- earliest_time: datetime.datetime,
1777
- latest_time: datetime.datetime,
1778
- ) -> None:
1779
- """
1780
- This function generates and ordered dictionary storing the data related to an event.
1781
-
1782
- It returns a dictionary. The keys are the start times of the
1783
- events. The values are also dictionaries containing:
1784
- - type: The type of the event.
1785
- - end: The end time of the event.
1786
- - area: The area id of the event.
1787
- - slot: The slot id of the event.
1788
- - id: The id of the event-related process.
1789
-
1790
- For trips, only the type is stored.
1791
-
1792
- For waiting events, the slot is not stored for now.
1793
-
1794
- :param current_vehicle: a :class:`eflips.depot.simple_vehicle.SimpleVehicle` object.
1795
-
1796
- :param virtual_waiting_area_id: the id of the virtual waiting area. Vehicles waiting for the first process will park here.
1797
-
1798
- :param earliest_time: the earliest relevant time of the current vehicle. Any events earlier than this will not be
1799
- handled.
1800
-
1801
- :param latest_time: the latest relevant time of the current vehicle. Any events later than this will not be handled.
1802
-
1803
- :return: None. The results are added to the dictionary.
1804
- """
1805
-
1806
- logger = logging.getLogger(__name__)
1807
-
1808
- # For convenience
1809
- area_log = current_vehicle.logger.loggedData["dwd.current_area"]
1810
- slot_log = current_vehicle.logger.loggedData["dwd.current_slot"]
1811
- waiting_log = current_vehicle.logger.loggedData["area_waiting_time"]
1812
-
1813
- # Handling waiting events
1814
- waiting_log_timekeys = sorted(waiting_log.keys())
1815
-
1816
- for idx in range(len(waiting_log_timekeys)):
1817
- waiting_end_time = waiting_log_timekeys[idx]
1818
-
1819
- # Only extract events if the time is within the upper mentioned range
1820
-
1821
- if earliest_time <= waiting_end_time <= latest_time:
1822
- waiting_info = waiting_log[waiting_end_time]
1823
-
1824
- if waiting_info["waiting_time"] == 0:
1825
- continue
1826
-
1827
- logger.info(
1828
- f"Vehicle {current_vehicle.ID} has been waiting for {waiting_info['waiting_time']} seconds. "
1829
- )
1830
-
1831
- start_time = waiting_end_time - waiting_info["waiting_time"]
1832
-
1833
- if waiting_info["area"] == waiting_log[waiting_log_timekeys[0]]["area"]:
1834
- # if the vehicle is waiting for the first process, put it in the virtual waiting area
1835
- waiting_area_id = virtual_waiting_area_id
1836
- else:
1837
- # If the vehicle is waiting for other processes,
1838
- # put it in the area of the prodecessor process of the waited process.
1839
- waiting_area_id = waiting_log[waiting_log_timekeys[idx - 1]]["area"]
1840
-
1841
- dict_of_events[start_time] = {
1842
- "type": "Standby",
1843
- "end": waiting_end_time,
1844
- "area": waiting_area_id,
1845
- "is_waiting": True,
1846
- }
1847
-
1848
- # Create a list of battery log in order of time asc. Convenient for looking up corresponding soc
1849
-
1850
- for time_stamp, process_log in current_vehicle.logger.loggedData[
1851
- "dwd.active_processes_copy"
1852
- ].items():
1853
- if earliest_time <= time_stamp <= latest_time:
1854
- num_process = len(process_log)
1855
- if num_process == 0:
1856
- # A departure happens and this trip should already be stored in the dictionary
1857
- pass
1858
- else:
1859
- for process in process_log:
1860
- current_area = area_log[time_stamp]
1861
- current_slot = slot_log[time_stamp]
1862
-
1863
- if current_area is None or current_slot is None:
1864
- raise ValueError(
1865
- f"For process {process.ID} Area and slot should not be None."
1866
- )
1867
-
1868
- match process.status:
1869
- case ProcessStatus.COMPLETED | ProcessStatus.CANCELLED:
1870
- assert (
1871
- len(process.starts) == 1 and len(process.ends) == 1
1872
- ), (
1873
- f"Current process {process.ID} is completed and should only contain one start and "
1874
- f"one end time."
1875
- )
1876
-
1877
- if process.dur > 0:
1878
- # Valid duration
1879
- dict_of_events[time_stamp] = {
1880
- "type": type(process).__name__,
1881
- "end": process.ends[0],
1882
- "area": current_area.ID,
1883
- "slot": current_slot,
1884
- "id": process.ID,
1885
- }
1886
- else:
1887
- # Duration is 0
1888
- assert current_area.issink is True, (
1889
- f"A process with no duration could only "
1890
- f"happen in the last area before dispatched"
1891
- )
1892
- if (
1893
- time_stamp in dict_of_events.keys()
1894
- and "end" in dict_of_events[time_stamp].keys()
1895
- ):
1896
- start_this_event = dict_of_events[time_stamp]["end"]
1897
- if start_this_event in dict_of_events.keys():
1898
- if (
1899
- dict_of_events[start_this_event]["type"]
1900
- == "Trip"
1901
- ):
1902
- logger.info(
1903
- f"Vehicle {current_vehicle.ID} must depart immediately after charged. "
1904
- f"Thus there will be no STANDBY_DEPARTURE event."
1905
- )
1906
-
1907
- else:
1908
- raise ValueError(
1909
- f"There is already an event "
1910
- f"{dict_of_events[start_this_event]} at {start_this_event}."
1911
- )
1912
-
1913
- continue
1914
-
1915
- dict_of_events[start_this_event] = {
1916
- "type": type(process).__name__,
1917
- "area": current_area.ID,
1918
- "slot": current_slot,
1919
- "id": process.ID,
1920
- }
1921
-
1922
- case ProcessStatus.IN_PROGRESS:
1923
- assert (
1924
- len(process.starts) == 1 and len(process.ends) == 0
1925
- ), f"Current process {process.ID} is marked IN_PROGRESS, but has an end."
1926
-
1927
- if current_area is None or current_slot is None:
1928
- raise ValueError(
1929
- f"For process {process.ID} Area and slot should not be None."
1930
- )
1931
-
1932
- if process.dur > 0:
1933
- # Valid duration
1934
- dict_of_events[time_stamp] = {
1935
- "type": type(process).__name__,
1936
- "end": process.etc,
1937
- "area": current_area.ID,
1938
- "slot": current_slot,
1939
- "id": process.ID,
1940
- }
1941
- else:
1942
- raise NotImplementedError(
1943
- "We believe this should never happen. If it happens, handle it here."
1944
- )
1945
-
1946
- # The following ProcessStatus possibly only happen while the simulation is running,
1947
- # not in the results
1948
- case ProcessStatus.WAITING:
1949
- raise NotImplementedError(
1950
- f"Current process {process.ID} is waiting. Not implemented yet."
1951
- )
1952
-
1953
- case ProcessStatus.NOT_STARTED:
1954
- raise NotImplementedError(
1955
- f"Current process {process.ID} is not started. Not implemented yet."
1956
- )
1957
-
1958
- case _:
1959
- raise ValueError(
1960
- f"Invalid process status {process.status} for process {process.ID}."
1961
- )
1962
-
1963
-
1964
- def _complete_standby_departure_events(
1965
- dict_of_events: Dict, latest_time: datetime.datetime
1966
- ) -> None:
1967
- """
1968
- This function completes the standby departure events by adding an end time to each standby departure event.
1969
-
1970
- :param dict_of_events: a dictionary containing the events of a vehicle. The keys are the start times of the events.
1971
-
1972
- :param latest_time: the latest relevant time of the current vehicle. Any events later than this will not be handled.
1973
-
1974
- :return: None. The results are added to the dictionary.
1975
- """
1976
- for i in range(len(dict_of_events.keys())):
1977
- time_keys = sorted(dict_of_events.keys())
1978
-
1979
- process_dict = dict_of_events[time_keys[i]]
1980
- if "end" not in process_dict and process_dict["type"] != "Trip":
1981
- # End time of a standby_departure will be the start of the following trip
1982
- if i == len(time_keys) - 1:
1983
- # The event reaches simulation end
1984
- end_time = latest_time
1985
- else:
1986
- end_time = time_keys[i + 1]
1987
-
1988
- process_dict["end"] = end_time
1989
-
1990
-
1991
- def _add_soc_to_events(dict_of_events, battery_log) -> None:
1992
- """
1993
- This function completes the soc of each event by looking up the battery log.
1994
-
1995
- :param dict_of_events: a dictionary containing the events of a vehicle. The keys are the start times of the events.
1996
-
1997
- :param battery_log: a list of battery logs of a vehicle.
1998
-
1999
- :return: None. The results are added to the dictionary.
2000
- """
2001
- battery_log_list = []
2002
- for log in battery_log:
2003
- battery_log_list.append((log.t, log.energy / log.energy_real))
2004
-
2005
- time_keys = sorted(dict_of_events.keys())
2006
- for i in range(len(time_keys)):
2007
- # Get soc
2008
- soc_start = None
2009
- soc_end = None
2010
- start_time = time_keys[i]
2011
- process_dict = dict_of_events[time_keys[i]]
2012
- for j in range(len(battery_log_list)):
2013
- # Access the correct battery log according to time since there is only one battery log for each time
2014
- log = battery_log_list[j]
2015
-
2016
- if process_dict["type"] != "Trip":
2017
- if log[0] == start_time:
2018
- soc_start = log[1]
2019
- if log[0] == process_dict["end"]:
2020
- soc_end = log[1]
2021
- if log[0] < start_time < battery_log_list[j + 1][0]:
2022
- soc_start = log[1]
2023
- if log[0] < process_dict["end"] < battery_log_list[j + 1][0]:
2024
- soc_end = log[1]
2025
-
2026
- if soc_start is not None:
2027
- soc_start = min(soc_start, 1) # so
2028
- process_dict["soc_start"] = soc_start
2029
- if soc_end is not None:
2030
- soc_end = min(soc_end, 1) # soc should not exceed 1
2031
- process_dict["soc_end"] = soc_end
2032
-
2033
- else:
2034
- continue
2035
-
2036
-
2037
- def _add_events_into_database(
2038
- db_vehicle, dict_of_events, session, scenario, simulation_start_time
2039
- ) -> None:
2040
- """
2041
- This function generates :class:`eflips.model.Event` objects from the dictionary of events and adds them into the.
2042
-
2043
- database.
2044
-
2045
- :param db_vehicle: vehicle object in the database
2046
-
2047
- :param dict_of_events: dictionary containing the events of a vehicle. The keys are the start times of the events.
2048
-
2049
- :param session: a :class:`sqlalchemy.orm.Session` object for database connection.
2050
-
2051
- :param scenario: the current simulated scenario
2052
-
2053
- :param simulation_start_time: simulation start time in :class:`datetime.datetime` format
2054
-
2055
- :return: None. The results are added to the database.
2056
- """
2057
- logger = logging.getLogger(__name__)
2058
-
2059
- for start_time, process_dict in dict_of_events.items():
2060
- # Generate EventType
2061
- match process_dict["type"]:
2062
- case "Serve":
2063
- event_type = EventType.SERVICE
2064
- case "Charge":
2065
- event_type = EventType.CHARGING_DEPOT
2066
- case "Standby":
2067
- if (
2068
- "is_waiting" in process_dict.keys()
2069
- and process_dict["is_waiting"] is True
2070
- ):
2071
- event_type = EventType.STANDBY
2072
- else:
2073
- event_type = EventType.STANDBY_DEPARTURE
2074
- case "Precondition":
2075
- event_type = EventType.PRECONDITIONING
2076
- case "Trip":
2077
- continue
2078
- case _:
2079
- raise ValueError(
2080
- 'Invalid process type %s. Valid process types are "Serve", "Charge", '
2081
- '"Standby", "Precondition"'
2082
- )
2083
-
2084
- if process_dict["end"] == start_time:
2085
- logger.warning("Refusing to create an event with zero duration.")
2086
- continue
2087
-
2088
- current_event = Event(
2089
- scenario=scenario,
2090
- vehicle_type_id=db_vehicle.vehicle_type_id,
2091
- vehicle=db_vehicle,
2092
- station_id=None,
2093
- area_id=int(process_dict["area"]),
2094
- subloc_no=int(process_dict["slot"]) - 1
2095
- if "slot" in process_dict.keys()
2096
- else 00,
2097
- trip_id=None,
2098
- time_start=timedelta(seconds=start_time) + simulation_start_time,
2099
- time_end=timedelta(seconds=process_dict["end"]) + simulation_start_time,
2100
- soc_start=process_dict["soc_start"]
2101
- if process_dict["soc_start"] is not None
2102
- else process_dict["soc_end"],
2103
- soc_end=process_dict["soc_end"]
2104
- if process_dict["soc_end"] is not None
2105
- else process_dict["soc_start"], # if only one battery log is found,
2106
- # then this is not an event with soc change
2107
- event_type=event_type,
2108
- description=process_dict["id"] if "id" in process_dict.keys() else None,
2109
- timeseries=None,
2110
- )
2111
-
2112
- session.add(current_event)
2113
-
2114
-
2115
- def _update_vehicle_in_rotation(session, scenario, list_of_assigned_schedules) -> None:
2116
- """
2117
- This function updates the vehicle id assigned to the rotations and deletes the events that are not depot events.
2118
-
2119
- :param session: a :class:`sqlalchemy.orm.Session` object for database connection.
2120
- :param scenario: the current simulated scenario
2121
- :param list_of_assigned_schedules: a list of tuples containing the rotation id and the vehicle id.
2122
- :return: None. The results are added to the database.
2123
- """
2124
- # New rotation assignment
2125
- for schedule_id, vehicle_id in list_of_assigned_schedules:
2126
- # Get corresponding old vehicle id
2127
- session.query(Rotation).filter(Rotation.id == schedule_id).update(
2128
- {"vehicle_id": vehicle_id}, synchronize_session="auto"
2129
- )
2130
-
2131
- # Delete all non-depot events
2132
- session.query(Event).filter(
2133
- Event.scenario == scenario,
2134
- Event.trip_id.isnot(None) | Event.station_id.isnot(None),
2135
- ).delete(synchronize_session="auto")
2136
-
2137
- session.flush()
2138
-
2139
- # Delete all vehicles without rotations
2140
- vehicle_assigned_sq = (
2141
- session.query(Rotation.vehicle_id)
2142
- .filter(Rotation.scenario == scenario)
2143
- .distinct()
2144
- .subquery()
2145
- )
2146
-
2147
- session.query(Vehicle).filter(Vehicle.scenario == scenario).filter(
2148
- Vehicle.id.not_in(select(vehicle_assigned_sq))
2149
- ).delete()
2150
-
2151
- session.flush()
2152
-
2153
-
2154
- def _update_waiting_events(session, scenario, waiting_area_id) -> None:
2155
- """
2156
- This function evaluates the capacity of waiting area and assigns the waiting events to corresponding slots in the.
2157
-
2158
- waiting area.
2159
-
2160
- :param session: a :class:`sqlalchemy.orm.Session` object for database connection.
2161
-
2162
- :param scenario: the current simulated scenario.
2163
-
2164
- :param waiting_area_id: id of the waiting area.
2165
-
2166
- :raise ValueError: if the waiting area capacity is less than the peak waiting occupancy.
2167
-
2168
- :return: None. The results are added to the database.
2169
- """
2170
- logger = logging.getLogger(__name__)
2171
-
2172
- # Process all the STANDBY (waiting) events #
2173
- all_waiting_starts = (
2174
- session.query(Event)
2175
- .filter(
2176
- Event.scenario_id == scenario.id,
2177
- Event.event_type == EventType.STANDBY,
2178
- Event.area_id == waiting_area_id,
2179
- )
2180
- .all()
2181
- )
2182
-
2183
- all_waiting_ends = (
2184
- session.query(Event)
2185
- .filter(
2186
- Event.scenario_id == scenario.id,
2187
- Event.event_type == EventType.STANDBY,
2188
- Event.area_id == waiting_area_id,
2189
- )
2190
- .all()
2191
- )
2192
-
2193
- assert len(all_waiting_starts) == len(
2194
- all_waiting_ends
2195
- ), f"Number of waiting events starts {len(all_waiting_starts)} is not equal to the number of waiting event ends"
2196
-
2197
- if len(all_waiting_starts) == 0:
2198
- logger.info(
2199
- "No waiting events found. The depot has enough capacity for waiting. Change the waiting area capacity to 10 as buffer."
2200
- )
2201
-
2202
- session.query(Area).filter(Area.id == waiting_area_id).update(
2203
- {"capacity": 10}, synchronize_session="auto"
2204
- )
2205
-
2206
- return
2207
-
2208
- list_waiting_timestamps = []
2209
- for waiting_start in all_waiting_starts:
2210
- list_waiting_timestamps.append(
2211
- {"timestamp": waiting_start.time_start, "event": (waiting_start.id, 1)}
2212
- )
2213
-
2214
- for waiting_end in all_waiting_ends:
2215
- list_waiting_timestamps.append(
2216
- {"timestamp": waiting_end.time_end, "event": (waiting_end.id, -1)}
2217
- )
2218
-
2219
- list_waiting_timestamps.sort(key=lambda x: x["timestamp"])
2220
- start_and_end_records = [wt["event"][1] for wt in list_waiting_timestamps]
2221
-
2222
- peak_waiting_occupancy = max(list(itertools.accumulate(start_and_end_records)))
2223
-
2224
- # Assuming that there is only one waiting area in each depot
2225
-
2226
- waiting_area_id = all_waiting_starts[0].area_id
2227
- waiting_area = session.query(Area).filter(Area.id == waiting_area_id).first()
2228
- if waiting_area.capacity > peak_waiting_occupancy:
2229
- logger.info(
2230
- f"Current waiting area capacity {waiting_area.capacity} "
2231
- f"is greater than the peak waiting occupancy. Updating the capacity to {peak_waiting_occupancy}."
2232
- )
2233
- session.query(Area).filter(Area.id == waiting_area_id).update(
2234
- {"capacity": peak_waiting_occupancy}, synchronize_session="auto"
2235
- )
2236
- session.flush()
2237
- elif waiting_area.capacity < peak_waiting_occupancy:
2238
- raise ValueError(
2239
- f"Waiting area capacity is less than the peak waiting occupancy. "
2240
- f"Waiting area capacity: {waiting_area.capacity}, peak waiting occupancy: {peak_waiting_occupancy}."
2241
- )
2242
- else:
2243
- pass
2244
-
2245
- session.flush()
2246
-
2247
- # Update waiting slots
2248
- virtual_waiting_area = [None] * peak_waiting_occupancy
2249
- for wt in list_waiting_timestamps:
2250
- # check in
2251
- if wt["event"][1] == 1:
2252
- for i in range(len(virtual_waiting_area)):
2253
- if virtual_waiting_area[i] is None:
2254
- virtual_waiting_area[i] = wt["event"][0]
2255
- session.query(Event).filter(Event.id == wt["event"][0]).update(
2256
- {"subloc_no": i}, synchronize_session="auto"
2257
- )
2258
- break
2259
- # check out
2260
- else:
2261
- for i in range(len(virtual_waiting_area)):
2262
- if virtual_waiting_area[i] == wt["event"][0]:
2263
- current_waiting_event = (
2264
- session.query(Event).filter(Event.id == wt["event"][0]).first()
2265
- )
2266
- assert current_waiting_event.subloc_no == i, (
2267
- f"Subloc number of the event {current_waiting_event.id} is not equal to the index of the "
2268
- f"event in the virtual waiting area."
2269
- )
2270
- virtual_waiting_area[i] = None
2271
- break
2272
-
2273
- session.flush()
1312
+ update_vehicle_in_rotation(session, scenario, list_of_assigned_schedules)
1313
+ update_waiting_events(session, scenario, waiting_area_id)