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.
- eflips/depot/api/__init__.py +19 -979
- eflips/depot/api/private/depot.py +360 -1
- eflips/depot/api/private/results_to_database.py +586 -0
- {eflips_depot-4.3.17.dist-info → eflips_depot-4.3.18.dist-info}/METADATA +2 -1
- {eflips_depot-4.3.17.dist-info → eflips_depot-4.3.18.dist-info}/RECORD +7 -6
- {eflips_depot-4.3.17.dist-info → eflips_depot-4.3.18.dist-info}/WHEEL +1 -1
- {eflips_depot-4.3.17.dist-info → eflips_depot-4.3.18.dist-info}/LICENSE.md +0 -0
eflips/depot/api/__init__.py
CHANGED
|
@@ -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
|
-
) =
|
|
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
|
-
|
|
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
|
-
|
|
1289
|
+
complete_standby_departure_events(dict_of_events, latest_time)
|
|
1674
1290
|
|
|
1675
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1697
|
-
|
|
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)
|