eflips-depot 4.3.4__py3-none-any.whl → 4.3.5__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.
- eflips/depot/api/__init__.py +728 -53
- eflips/depot/api/private/depot.py +83 -8
- eflips/depot/api/private/smart_charging.py +3 -20
- eflips/depot/api/private/util.py +17 -3
- {eflips_depot-4.3.4.dist-info → eflips_depot-4.3.5.dist-info}/METADATA +1 -1
- {eflips_depot-4.3.4.dist-info → eflips_depot-4.3.5.dist-info}/RECORD +8 -8
- {eflips_depot-4.3.4.dist-info → eflips_depot-4.3.5.dist-info}/LICENSE.md +0 -0
- {eflips_depot-4.3.4.dist-info → eflips_depot-4.3.5.dist-info}/WHEEL +0 -0
eflips/depot/api/__init__.py
CHANGED
|
@@ -31,29 +31,44 @@ import logging
|
|
|
31
31
|
import os
|
|
32
32
|
import warnings
|
|
33
33
|
from collections import OrderedDict
|
|
34
|
+
from dataclasses import dataclass
|
|
34
35
|
from datetime import timedelta
|
|
35
36
|
from enum import Enum
|
|
36
37
|
from math import ceil
|
|
37
|
-
from
|
|
38
|
+
from eflips.model import Process
|
|
39
|
+
from typing import Any, Dict, Optional, Tuple, Union, List
|
|
38
40
|
|
|
39
41
|
import numpy as np
|
|
40
42
|
import sqlalchemy.orm
|
|
41
43
|
from eflips.model import (
|
|
42
44
|
Area,
|
|
45
|
+
AreaType,
|
|
46
|
+
AssocPlanProcess,
|
|
43
47
|
Depot,
|
|
44
48
|
Event,
|
|
45
49
|
EventType,
|
|
50
|
+
Plan,
|
|
46
51
|
Rotation,
|
|
47
52
|
Scenario,
|
|
53
|
+
Station,
|
|
48
54
|
Trip,
|
|
49
55
|
Vehicle,
|
|
56
|
+
VehicleType,
|
|
50
57
|
)
|
|
51
58
|
from sqlalchemy.orm import Session
|
|
52
59
|
from sqlalchemy.sql import select
|
|
53
60
|
|
|
54
61
|
import eflips.depot
|
|
55
|
-
from eflips.depot import
|
|
62
|
+
from eflips.depot import (
|
|
63
|
+
DepotEvaluation,
|
|
64
|
+
DirectArea,
|
|
65
|
+
LineArea,
|
|
66
|
+
ProcessStatus,
|
|
67
|
+
SimulationHost,
|
|
68
|
+
SimpleVehicle,
|
|
69
|
+
)
|
|
56
70
|
from eflips.depot.api.private.depot import (
|
|
71
|
+
_generate_all_direct_depot,
|
|
57
72
|
create_simple_depot,
|
|
58
73
|
delete_depots,
|
|
59
74
|
depot_to_template,
|
|
@@ -68,6 +83,7 @@ from eflips.depot.api.private.util import (
|
|
|
68
83
|
VehicleSchedule,
|
|
69
84
|
check_depot_validity,
|
|
70
85
|
)
|
|
86
|
+
from eflips.depot.evaluation import to_prev_values
|
|
71
87
|
|
|
72
88
|
|
|
73
89
|
class SmartChargingStrategy(Enum):
|
|
@@ -370,12 +386,644 @@ def simple_consumption_simulation(
|
|
|
370
386
|
session.add(current_event)
|
|
371
387
|
|
|
372
388
|
|
|
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
|
+
@dataclass
|
|
615
|
+
class DepotConfiguration:
|
|
616
|
+
charging_power: float
|
|
617
|
+
line_counts: Dict[VehicleType, int]
|
|
618
|
+
direct_counts: Dict[VehicleType, int]
|
|
619
|
+
clean_duration: int
|
|
620
|
+
num_clean_areas: int
|
|
621
|
+
num_shunting_areas: int
|
|
622
|
+
|
|
623
|
+
|
|
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
|
+
def generate_realistic_depot_layout(
|
|
755
|
+
scenario: Union[Scenario, int, Any],
|
|
756
|
+
charging_power: float,
|
|
757
|
+
database_url: Optional[str] = None,
|
|
758
|
+
delete_existing_depot: bool = False,
|
|
759
|
+
line_length: int = 8,
|
|
760
|
+
CLEAN_DURATION: int = 10 * 60, # 10 minutes in seconds
|
|
761
|
+
shunting_duration: timedelta = timedelta(minutes=5),
|
|
762
|
+
) -> DepotConfiguration:
|
|
763
|
+
"""
|
|
764
|
+
Creates a realistic depot layout for the scenario.
|
|
765
|
+
|
|
766
|
+
This is done by starting with an all direct depot layout,
|
|
767
|
+
looking at the vehicle count, creating an "all line" layout and then turning some of these lines into direct
|
|
768
|
+
areas until the vehicle count of the all direct depot layout (+ an allowance) is reached.
|
|
769
|
+
|
|
770
|
+
:param scenario: The scenario for which the depot layout should be generated.
|
|
771
|
+
:param charging_power: The charging power for the line areas in kW.
|
|
772
|
+
:param database_url: An optional database URL. If no database URL is passed and the `scenario` parameter is not a
|
|
773
|
+
:class:`eflips.model.Scenario` object, the environment variable `DATABASE_URL` must be set to a valid database
|
|
774
|
+
URL.
|
|
775
|
+
:param delete_existing_depot: Whether to delete an existing depot layout for this scenario. If set to False and a
|
|
776
|
+
depot layout already exists, a ValueError will be raised.
|
|
777
|
+
:param charging_power_direct: The charging power for the direct areas in kW. If not set, the charging power for the
|
|
778
|
+
line areas will be used.
|
|
779
|
+
|
|
780
|
+
:return: None. The depot layout will be added to the database.
|
|
781
|
+
"""
|
|
782
|
+
logging.basicConfig(level=logging.DEBUG) # TODO: Remove this line
|
|
783
|
+
logger = logging.getLogger(__name__)
|
|
784
|
+
|
|
785
|
+
with create_session(scenario, database_url) as (session, scenario):
|
|
786
|
+
# STEP 0: Delete existing depots if asked to, raise an Exception otherwise
|
|
787
|
+
if session.query(Depot).filter(Depot.scenario_id == scenario.id).count() != 0:
|
|
788
|
+
if delete_existing_depot is False:
|
|
789
|
+
raise ValueError("Depot already exists.")
|
|
790
|
+
delete_depots(scenario, session)
|
|
791
|
+
|
|
792
|
+
# Make sure that the consumption simulation has been run
|
|
793
|
+
if session.query(Event).filter(Event.scenario_id == scenario.id).count() == 0:
|
|
794
|
+
raise ValueError(
|
|
795
|
+
"No consumption simulation found. Please run the consumption simulation first."
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
# STEP 2: Identify the spots where we will put a depot
|
|
799
|
+
# Identify all the spots that serve as start *and* end of a rotation
|
|
800
|
+
depot_stations_and_vehicle_types = group_rotations_by_start_end_stop(
|
|
801
|
+
scenario.id, session
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
# STEP 3: Put "all direct" depots at these spots and find the vehicle counts
|
|
805
|
+
depot_stations = []
|
|
806
|
+
for (
|
|
807
|
+
first_last_stop_tup,
|
|
808
|
+
vehicle_type_rotation_dict,
|
|
809
|
+
) in group_rotations_by_start_end_stop(scenario.id, session).items():
|
|
810
|
+
first_stop, last_stop = first_last_stop_tup
|
|
811
|
+
if first_stop != last_stop:
|
|
812
|
+
raise ValueError("First and last stop of a rotation are not the same.")
|
|
813
|
+
depot_stations.append(first_stop)
|
|
814
|
+
|
|
815
|
+
all_direct_counts: Dict[
|
|
816
|
+
Station, Dict[VehicleType, int]
|
|
817
|
+
] = vehicle_counts_for_direct_layout(
|
|
818
|
+
CLEAN_DURATION=CLEAN_DURATION,
|
|
819
|
+
charging_power=charging_power,
|
|
820
|
+
stations=depot_stations,
|
|
821
|
+
scenario=scenario,
|
|
822
|
+
session=session,
|
|
823
|
+
vehicle_type_dict=vehicle_type_rotation_dict,
|
|
824
|
+
shunting_duration=shunting_duration,
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
# STEP 4: Run the simulation with depots that also have a lot of line areas
|
|
828
|
+
# I know I could probably skip step 3 and go directly to step 4, but that's how I got it working and
|
|
829
|
+
# I'm too lazy to change it now
|
|
830
|
+
|
|
831
|
+
for station, vehicle_type_and_counts in all_direct_counts.items():
|
|
832
|
+
line_counts: Dict[VehicleType, int] = dict()
|
|
833
|
+
direct_counts: Dict[VehicleType, int] = dict()
|
|
834
|
+
|
|
835
|
+
# Create a Depot that has a lot of line areas as well
|
|
836
|
+
for vehicle_type, count in vehicle_type_and_counts.items():
|
|
837
|
+
line_counts[vehicle_type] = ceil(count / line_length)
|
|
838
|
+
direct_counts[vehicle_type] = ceil(count) + 500
|
|
839
|
+
|
|
840
|
+
# Run the simulation with this depot
|
|
841
|
+
generate_line_depot_layout(
|
|
842
|
+
CLEAN_DURATION=CLEAN_DURATION,
|
|
843
|
+
charging_power=charging_power,
|
|
844
|
+
station=station,
|
|
845
|
+
scenario=scenario,
|
|
846
|
+
session=session,
|
|
847
|
+
direct_counts=direct_counts,
|
|
848
|
+
line_counts=line_counts,
|
|
849
|
+
line_length=line_length,
|
|
850
|
+
vehicle_type_rotation_dict=vehicle_type_rotation_dict,
|
|
851
|
+
shunting_duration=shunting_duration,
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
# Simulate the depot
|
|
855
|
+
# We will not be using add_evaluation_to_database instead taking the vehicle counts directly from the `ev` object
|
|
856
|
+
logger.info("Simulating the scenario")
|
|
857
|
+
logger.info("1/2: Initializing the simulation host")
|
|
858
|
+
simulation_host = init_simulation(
|
|
859
|
+
scenario=scenario,
|
|
860
|
+
session=session,
|
|
861
|
+
)
|
|
862
|
+
logger.info("2/2: Running the simulation")
|
|
863
|
+
depot_evaluations = run_simulation(simulation_host)
|
|
864
|
+
|
|
865
|
+
# We need to remember the depot-id-station mapping
|
|
866
|
+
depot_id_station_mapping: Dict[str, Station] = dict()
|
|
867
|
+
for depot_id_as_str, ev in depot_evaluations.items():
|
|
868
|
+
station = (
|
|
869
|
+
session.query(Station)
|
|
870
|
+
.join(Depot)
|
|
871
|
+
.filter(Depot.id == int(depot_id_as_str))
|
|
872
|
+
.one()
|
|
873
|
+
)
|
|
874
|
+
depot_id_station_mapping[depot_id_as_str] = station
|
|
875
|
+
|
|
876
|
+
# Delete the old depot
|
|
877
|
+
delete_depots(scenario, session)
|
|
878
|
+
|
|
879
|
+
for depot_id_as_str, ev in depot_evaluations.items():
|
|
880
|
+
assert isinstance(ev, DepotEvaluation)
|
|
881
|
+
|
|
882
|
+
if False:
|
|
883
|
+
ev.path_results = depot_id_as_str
|
|
884
|
+
os.makedirs(depot_id_as_str, exist_ok=True)
|
|
885
|
+
|
|
886
|
+
ev.vehicle_periods(
|
|
887
|
+
periods={
|
|
888
|
+
"depot general": "darkgray",
|
|
889
|
+
"park": "lightgray",
|
|
890
|
+
"Arrival Cleaning": "steelblue",
|
|
891
|
+
"Charging": "forestgreen",
|
|
892
|
+
"Standby Pre-departure": "darkblue",
|
|
893
|
+
"precondition": "black",
|
|
894
|
+
"trip": "wheat",
|
|
895
|
+
},
|
|
896
|
+
save=True,
|
|
897
|
+
show=False,
|
|
898
|
+
formats=(
|
|
899
|
+
"pdf",
|
|
900
|
+
"png",
|
|
901
|
+
),
|
|
902
|
+
show_total_power=True,
|
|
903
|
+
show_annotates=True,
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
# Find the actual utilization.
|
|
907
|
+
utilization: Dict[str, int] = real_peak_area_utilization(ev)
|
|
908
|
+
utilization = {
|
|
909
|
+
session.query(VehicleType).filter(VehicleType.id == int(k)).one(): v
|
|
910
|
+
for k, v in utilization.items()
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
# Turn utilization into a two dictionaries, one for line areas and one for direct areas
|
|
914
|
+
for vehicle_type, counts in utilization.items():
|
|
915
|
+
line_counts[vehicle_type] = counts[AreaType.LINE]
|
|
916
|
+
direct_counts[vehicle_type] = counts[AreaType.DIRECT_ONESIDE] + 100
|
|
917
|
+
|
|
918
|
+
station = depot_id_station_mapping[depot_id_as_str]
|
|
919
|
+
|
|
920
|
+
generate_line_depot_layout(
|
|
921
|
+
CLEAN_DURATION=CLEAN_DURATION,
|
|
922
|
+
charging_power=charging_power,
|
|
923
|
+
station=station,
|
|
924
|
+
scenario=scenario,
|
|
925
|
+
session=session,
|
|
926
|
+
direct_counts=direct_counts,
|
|
927
|
+
line_counts=line_counts,
|
|
928
|
+
line_length=line_length,
|
|
929
|
+
vehicle_type_rotation_dict=vehicle_type_rotation_dict,
|
|
930
|
+
shunting_duration=shunting_duration,
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def vehicle_counts_for_direct_layout(
|
|
935
|
+
CLEAN_DURATION: int,
|
|
936
|
+
charging_power: float,
|
|
937
|
+
stations: List[Station],
|
|
938
|
+
scenario: Scenario,
|
|
939
|
+
session: sqlalchemy.orm.session.Session,
|
|
940
|
+
vehicle_type_dict: Dict[VehicleType, List[Rotation]],
|
|
941
|
+
shunting_duration: timedelta = timedelta(minutes=5),
|
|
942
|
+
) -> Dict[Station, Dict[VehicleType, int]]:
|
|
943
|
+
"""
|
|
944
|
+
Generate a simple depot, simulate it and return the number of vehicles for each vehicle type.
|
|
945
|
+
|
|
946
|
+
Do this for each depot station in the scenario.
|
|
947
|
+
:param CLEAN_DURATION: The duration of the cleaning process in seconds.
|
|
948
|
+
:param charging_power: The charging power of the charging area in kW.
|
|
949
|
+
:param station: The stop where the depot is located.
|
|
950
|
+
:param scenario: The scenario for which the depot layout should be generated.
|
|
951
|
+
:param session: The SQLAlchemy session object.
|
|
952
|
+
:param vehicle_type_dict: A dictionary with vehicle types as keys and rotations as values.
|
|
953
|
+
:return: A dictionary with vehicle types as keys and the number of vehicles as values.
|
|
954
|
+
"""
|
|
955
|
+
logger = logging.getLogger(__name__)
|
|
956
|
+
|
|
957
|
+
for station in stations:
|
|
958
|
+
logger.info(f"Generating all direct depot layout at {station.name}")
|
|
959
|
+
# Generate the depot
|
|
960
|
+
direct_counts = {}
|
|
961
|
+
line_counts = {}
|
|
962
|
+
for vehicle_type, rotations in vehicle_type_dict.items():
|
|
963
|
+
direct_counts[vehicle_type] = len(rotations)
|
|
964
|
+
line_counts[vehicle_type] = 1
|
|
965
|
+
|
|
966
|
+
generate_line_depot_layout(
|
|
967
|
+
CLEAN_DURATION=CLEAN_DURATION,
|
|
968
|
+
charging_power=charging_power,
|
|
969
|
+
station=station,
|
|
970
|
+
scenario=scenario,
|
|
971
|
+
session=session,
|
|
972
|
+
direct_counts=direct_counts,
|
|
973
|
+
line_counts=line_counts,
|
|
974
|
+
line_length=8, # We don't care about the line length here
|
|
975
|
+
vehicle_type_rotation_dict=vehicle_type_dict,
|
|
976
|
+
shunting_duration=shunting_duration,
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
# Simulate the scenario
|
|
980
|
+
# We will not be using add_evaluation_to_database instead taking the vehicle counts directly from the `ev` object
|
|
981
|
+
logger.info("Simulating the scenario")
|
|
982
|
+
logger.info("1/2: Initializing the simulation host")
|
|
983
|
+
simulation_host = init_simulation(
|
|
984
|
+
scenario=scenario,
|
|
985
|
+
session=session,
|
|
986
|
+
)
|
|
987
|
+
logger.info("2/2: Running the simulation")
|
|
988
|
+
depot_evaluations = run_simulation(simulation_host)
|
|
989
|
+
|
|
990
|
+
assert len(depot_evaluations) == len(stations)
|
|
991
|
+
depot_evaluations: Dict[str, DepotEvaluation]
|
|
992
|
+
|
|
993
|
+
ret_val: Dict[Station, Dict[VehicleType, int]] = dict()
|
|
994
|
+
|
|
995
|
+
for depot_id_as_str, ev in depot_evaluations.items():
|
|
996
|
+
ev = next(iter(depot_evaluations.values()))
|
|
997
|
+
assert isinstance(ev, DepotEvaluation)
|
|
998
|
+
counts: Dict[str, int] = real_peak_vehicle_count(ev)
|
|
999
|
+
# The key of the dictionary is the vehicle type ID as a string. We need to convert it to a vehicle type object
|
|
1000
|
+
vehicle_type_dict = {
|
|
1001
|
+
session.query(VehicleType).filter(VehicleType.id == int(k)).one(): v
|
|
1002
|
+
for k, v in counts.items()
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
# Find the station object
|
|
1006
|
+
station = (
|
|
1007
|
+
session.query(Station)
|
|
1008
|
+
.join(Depot)
|
|
1009
|
+
.filter(Depot.id == int(depot_id_as_str))
|
|
1010
|
+
.one()
|
|
1011
|
+
)
|
|
1012
|
+
|
|
1013
|
+
ret_val[station] = vehicle_type_dict
|
|
1014
|
+
|
|
1015
|
+
# Delete the old depots
|
|
1016
|
+
delete_depots(scenario, session)
|
|
1017
|
+
|
|
1018
|
+
return ret_val
|
|
1019
|
+
|
|
1020
|
+
|
|
373
1021
|
def generate_depot_layout(
|
|
374
1022
|
scenario: Union[Scenario, int, Any],
|
|
375
1023
|
charging_power: float = 150,
|
|
376
1024
|
database_url: Optional[str] = None,
|
|
377
1025
|
delete_existing_depot: bool = False,
|
|
378
|
-
):
|
|
1026
|
+
) -> None:
|
|
379
1027
|
"""
|
|
380
1028
|
Generates one or more depots for the scenario.
|
|
381
1029
|
|
|
@@ -422,51 +1070,13 @@ def generate_depot_layout(
|
|
|
422
1070
|
first_stop, last_stop = first_last_stop_tup
|
|
423
1071
|
if first_stop != last_stop:
|
|
424
1072
|
raise ValueError("First and last stop of a rotation are not the same.")
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
end_time = max(
|
|
433
|
-
[rotation.trips[-1].arrival_time for rotation in rotations]
|
|
434
|
-
).timestamp()
|
|
435
|
-
timestamps_to_sample = np.arange(start_time, end_time, 60)
|
|
436
|
-
occupancy = np.zeros_like(timestamps_to_sample)
|
|
437
|
-
clean_occupancy = np.zeros_like(timestamps_to_sample)
|
|
438
|
-
for rotation in rotations:
|
|
439
|
-
rotation_start = rotation.trips[0].departure_time.timestamp()
|
|
440
|
-
rotation_end = rotation.trips[-1].arrival_time.timestamp()
|
|
441
|
-
occupancy += np.interp(
|
|
442
|
-
timestamps_to_sample,
|
|
443
|
-
[rotation_start, rotation_end],
|
|
444
|
-
[1, 1],
|
|
445
|
-
left=0,
|
|
446
|
-
right=0,
|
|
447
|
-
)
|
|
448
|
-
clean_occupancy += np.interp(
|
|
449
|
-
timestamps_to_sample,
|
|
450
|
-
[rotation_end, rotation_end + CLEAN_DURATION],
|
|
451
|
-
[1, 1],
|
|
452
|
-
left=0,
|
|
453
|
-
right=0,
|
|
454
|
-
)
|
|
455
|
-
max_occupancies[vehicle_type] = max(
|
|
456
|
-
max(occupancy), 1
|
|
457
|
-
) # To avoid zero occupancy
|
|
458
|
-
max_clean_occupancies[vehicle_type] = max(max(clean_occupancy), 1)
|
|
459
|
-
|
|
460
|
-
# Create a simple depot at this station
|
|
461
|
-
create_simple_depot(
|
|
462
|
-
scenario=scenario,
|
|
463
|
-
station=first_stop,
|
|
464
|
-
charging_capacities=max_occupancies,
|
|
465
|
-
cleaning_capacities=max_clean_occupancies,
|
|
466
|
-
charging_power=charging_power,
|
|
467
|
-
session=session,
|
|
468
|
-
cleaning_duration=timedelta(seconds=CLEAN_DURATION),
|
|
469
|
-
safety_margin=0.2,
|
|
1073
|
+
_generate_all_direct_depot(
|
|
1074
|
+
CLEAN_DURATION,
|
|
1075
|
+
charging_power,
|
|
1076
|
+
first_stop,
|
|
1077
|
+
scenario,
|
|
1078
|
+
session,
|
|
1079
|
+
vehicle_type_dict,
|
|
470
1080
|
)
|
|
471
1081
|
|
|
472
1082
|
|
|
@@ -668,6 +1278,7 @@ def init_simulation(
|
|
|
668
1278
|
:return: A :class:`eflips.depot.Simulation.SimulationHost` object. This object should be reagrded as a "black box"
|
|
669
1279
|
by the user. It should be passed to :func:`run_simulation()` to run the simulation and obtain the results.
|
|
670
1280
|
"""
|
|
1281
|
+
|
|
671
1282
|
# Clear the eflips settings
|
|
672
1283
|
eflips.settings.reset_settings()
|
|
673
1284
|
|
|
@@ -675,7 +1286,7 @@ def init_simulation(
|
|
|
675
1286
|
|
|
676
1287
|
# Step 1: Set up the depot
|
|
677
1288
|
eflips_depots = []
|
|
678
|
-
for depot in scenario.
|
|
1289
|
+
for depot in session.query(Depot).filter(Depot.scenario_id == scenario.id).all():
|
|
679
1290
|
# Step 1.5: Check validity of a depot
|
|
680
1291
|
check_depot_validity(depot)
|
|
681
1292
|
|
|
@@ -708,7 +1319,9 @@ def init_simulation(
|
|
|
708
1319
|
scenario=scenario,
|
|
709
1320
|
session=session,
|
|
710
1321
|
)
|
|
711
|
-
for rotation in
|
|
1322
|
+
for rotation in session.query(Rotation)
|
|
1323
|
+
.filter(Rotation.scenario_id == scenario.id)
|
|
1324
|
+
.all()
|
|
712
1325
|
]
|
|
713
1326
|
|
|
714
1327
|
first_departure_time = min(
|
|
@@ -746,8 +1359,11 @@ def init_simulation(
|
|
|
746
1359
|
simulation_host.timetable = timetable
|
|
747
1360
|
|
|
748
1361
|
# Step 4: Set up the vehicle types
|
|
1362
|
+
# Clear old vehicle counts, if they exist
|
|
1363
|
+
eflips.globalConstants["depot"]["vehicle_count"] = {}
|
|
1364
|
+
|
|
749
1365
|
# We need to calculate roughly how many vehicles we need for each depot
|
|
750
|
-
for depot in scenario.
|
|
1366
|
+
for depot in session.query(Depot).filter(Depot.scenario_id == scenario.id).all():
|
|
751
1367
|
depot_id = str(depot.id)
|
|
752
1368
|
eflips.globalConstants["depot"]["vehicle_count"][depot_id] = {}
|
|
753
1369
|
vehicle_types_for_depot = set(str(area.vehicle_type_id) for area in depot.areas)
|
|
@@ -784,7 +1400,9 @@ def init_simulation(
|
|
|
784
1400
|
] = (vehicle_count * 2)
|
|
785
1401
|
|
|
786
1402
|
# We need to put the vehicle type objects into the GlobalConstants
|
|
787
|
-
for vehicle_type in
|
|
1403
|
+
for vehicle_type in (
|
|
1404
|
+
session.query(VehicleType).filter(VehicleType.scenario_id == scenario.id).all()
|
|
1405
|
+
):
|
|
788
1406
|
eflips.globalConstants["depot"]["vehicle_types"][
|
|
789
1407
|
str(vehicle_type.id)
|
|
790
1408
|
] = vehicle_type_to_global_constants_dict(vehicle_type)
|
|
@@ -870,6 +1488,57 @@ def _add_evaluation_to_database(
|
|
|
870
1488
|
)
|
|
871
1489
|
|
|
872
1490
|
|
|
1491
|
+
def insert_dummy_standby_departure_events(depot_id: int, session: Session) -> None:
|
|
1492
|
+
"""
|
|
1493
|
+
Workaround for the missing STANDBY_DEPARTURE events in the database.
|
|
1494
|
+
|
|
1495
|
+
:param session: The database session
|
|
1496
|
+
:param scenario: A scenario object
|
|
1497
|
+
:return:
|
|
1498
|
+
"""
|
|
1499
|
+
logger = logging.getLogger(__name__)
|
|
1500
|
+
|
|
1501
|
+
# Look for charging events at areas belonging to the depot
|
|
1502
|
+
charging_events = (
|
|
1503
|
+
session.query(Event)
|
|
1504
|
+
.join(Area)
|
|
1505
|
+
.filter(Area.depot_id == depot_id)
|
|
1506
|
+
.filter(Event.event_type == EventType.CHARGING_DEPOT)
|
|
1507
|
+
.all()
|
|
1508
|
+
)
|
|
1509
|
+
|
|
1510
|
+
for charging_event in charging_events:
|
|
1511
|
+
# See if the next event is a DRIVING event, but there is time between the two events
|
|
1512
|
+
next_event = (
|
|
1513
|
+
session.query(Event)
|
|
1514
|
+
.filter(Event.time_start >= charging_event.time_end)
|
|
1515
|
+
.filter(Event.vehicle_id == charging_event.vehicle_id)
|
|
1516
|
+
.order_by(Event.time_start)
|
|
1517
|
+
.first()
|
|
1518
|
+
)
|
|
1519
|
+
if (
|
|
1520
|
+
next_event is not None
|
|
1521
|
+
and next_event.event_type == EventType.DRIVING
|
|
1522
|
+
and (next_event.time_start - charging_event.time_end) > timedelta(seconds=1)
|
|
1523
|
+
):
|
|
1524
|
+
logger.warning("Inserting dummy STANDBY_DEPARTURE event")
|
|
1525
|
+
# Insert a dummy STANDBY_DEPARTURE event
|
|
1526
|
+
dummy_event = Event(
|
|
1527
|
+
vehicle_id=charging_event.vehicle_id,
|
|
1528
|
+
vehicle_type_id=charging_event.vehicle.vehicle_type_id,
|
|
1529
|
+
time_start=charging_event.time_end,
|
|
1530
|
+
time_end=(next_event.time_start - timedelta(seconds=1)),
|
|
1531
|
+
event_type=EventType.STANDBY_DEPARTURE,
|
|
1532
|
+
area_id=charging_event.area_id,
|
|
1533
|
+
subloc_no=charging_event.subloc_no,
|
|
1534
|
+
scenario_id=charging_event.scenario_id,
|
|
1535
|
+
soc_start=charging_event.soc_end,
|
|
1536
|
+
soc_end=charging_event.soc_end,
|
|
1537
|
+
description="Dummy STANDBY_DEPARTURE event",
|
|
1538
|
+
)
|
|
1539
|
+
session.add(dummy_event)
|
|
1540
|
+
|
|
1541
|
+
|
|
873
1542
|
def add_evaluation_to_database(
|
|
874
1543
|
scenario: Scenario,
|
|
875
1544
|
depot_evaluations: Dict[str, DepotEvaluation],
|
|
@@ -903,7 +1572,7 @@ def add_evaluation_to_database(
|
|
|
903
1572
|
|
|
904
1573
|
waiting_area_id = None
|
|
905
1574
|
|
|
906
|
-
total_areas = scenario.
|
|
1575
|
+
total_areas = session.query(Area).filter(Area.scenario_id == scenario.id).all()
|
|
907
1576
|
for area in total_areas:
|
|
908
1577
|
if area.depot_id == int(depot_id) and len(area.processes) == 0:
|
|
909
1578
|
waiting_area_id = area.id
|
|
@@ -1352,6 +2021,8 @@ def _add_events_into_database(
|
|
|
1352
2021
|
|
|
1353
2022
|
:return: None. The results are added to the database.
|
|
1354
2023
|
"""
|
|
2024
|
+
logger = logging.getLogger(__name__)
|
|
2025
|
+
|
|
1355
2026
|
for start_time, process_dict in dict_of_events.items():
|
|
1356
2027
|
# Generate EventType
|
|
1357
2028
|
match process_dict["type"]:
|
|
@@ -1377,6 +2048,10 @@ def _add_events_into_database(
|
|
|
1377
2048
|
'"Standby", "Precondition"'
|
|
1378
2049
|
)
|
|
1379
2050
|
|
|
2051
|
+
if process_dict["end"] == start_time:
|
|
2052
|
+
logger.warning("Refusing to create an event with zero duration.")
|
|
2053
|
+
continue
|
|
2054
|
+
|
|
1380
2055
|
current_event = Event(
|
|
1381
2056
|
scenario=scenario,
|
|
1382
2057
|
vehicle_type_id=db_vehicle.vehicle_type_id,
|
|
@@ -5,6 +5,9 @@ from enum import Enum, auto
|
|
|
5
5
|
from math import ceil
|
|
6
6
|
from typing import Dict, List, Tuple
|
|
7
7
|
|
|
8
|
+
import eflips.model
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
8
11
|
import sqlalchemy.orm
|
|
9
12
|
from eflips.model import (
|
|
10
13
|
Scenario,
|
|
@@ -99,12 +102,15 @@ def depot_to_template(depot: Depot) -> Dict[str, str | Dict[str, str | int]]:
|
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
# Fill in vehicle_filter.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
"
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
# If the vehicle type id is set, the area is only for this vehicle type
|
|
106
|
+
if area.vehicle_type_id is not None:
|
|
107
|
+
template["areas"][area_name]["entry_filter"] = {
|
|
108
|
+
"filter_names": ["vehicle_type"],
|
|
109
|
+
"vehicle_types": [str(area.vehicle_type_id)],
|
|
110
|
+
}
|
|
111
|
+
else:
|
|
112
|
+
# If the vehicle type id is not set, the area is for all vehicle types
|
|
113
|
+
template["areas"][area_name]["entry_filter"] = dict()
|
|
108
114
|
|
|
109
115
|
for process in area.processes:
|
|
110
116
|
# Add process into process list
|
|
@@ -298,6 +304,7 @@ def create_simple_depot(
|
|
|
298
304
|
session: sqlalchemy.orm.session.Session,
|
|
299
305
|
cleaning_duration: timedelta = timedelta(minutes=30),
|
|
300
306
|
safety_margin: float = 0.0,
|
|
307
|
+
shunting_duration: timedelta = timedelta(minutes=5),
|
|
301
308
|
) -> None:
|
|
302
309
|
"""
|
|
303
310
|
Creates a simple depot for a given scenario.
|
|
@@ -336,7 +343,7 @@ def create_simple_depot(
|
|
|
336
343
|
name="Shunting 1",
|
|
337
344
|
scenario=scenario,
|
|
338
345
|
dispatchable=False,
|
|
339
|
-
duration=
|
|
346
|
+
duration=shunting_duration,
|
|
340
347
|
)
|
|
341
348
|
clean = Process(
|
|
342
349
|
name="Arrival Cleaning",
|
|
@@ -348,7 +355,7 @@ def create_simple_depot(
|
|
|
348
355
|
name="Shunting 2",
|
|
349
356
|
scenario=scenario,
|
|
350
357
|
dispatchable=False,
|
|
351
|
-
duration=
|
|
358
|
+
duration=shunting_duration,
|
|
352
359
|
)
|
|
353
360
|
charging = Process(
|
|
354
361
|
name="Charging",
|
|
@@ -505,3 +512,71 @@ def process_type(p: Process) -> ProcessType:
|
|
|
505
512
|
return ProcessType.STANDBY
|
|
506
513
|
else:
|
|
507
514
|
raise ValueError("Invalid process type")
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _generate_all_direct_depot(
|
|
518
|
+
CLEAN_DURATION: int,
|
|
519
|
+
charging_power: float,
|
|
520
|
+
first_stop: Station,
|
|
521
|
+
scenario: Scenario,
|
|
522
|
+
session: sqlalchemy.orm.session.Session,
|
|
523
|
+
vehicle_type_dict: Dict[VehicleType, List[Rotation]],
|
|
524
|
+
shunting_duration: timedelta = timedelta(minutes=5),
|
|
525
|
+
) -> None:
|
|
526
|
+
"""
|
|
527
|
+
Private inner function to generate a depot layout with an arrival and a charging area for each vehicle type.
|
|
528
|
+
|
|
529
|
+
:param CLEAN_DURATION: The duration of the cleaning process in seconds.
|
|
530
|
+
:param charging_power: The charging power of the charging area in kW.
|
|
531
|
+
:param first_stop: The stop where the depot is located.
|
|
532
|
+
:param scenario: The scenario for which the depot layout should be generated.
|
|
533
|
+
:param session: The SQLAlchemy session object.
|
|
534
|
+
:param vehicle_type_dict: A dictionary with vehicle types as keys and rotations as values.
|
|
535
|
+
:return: Nothing. The depot layout is created in the database.
|
|
536
|
+
"""
|
|
537
|
+
max_occupancies: Dict[eflips.model.VehicleType, int] = {}
|
|
538
|
+
max_clean_occupancies: Dict[eflips.model.VehicleType, int] = {}
|
|
539
|
+
for vehicle_type, rotations in vehicle_type_dict.items():
|
|
540
|
+
# Slightly convoluted vehicle summation
|
|
541
|
+
start_time = min(
|
|
542
|
+
[rotation.trips[0].departure_time for rotation in rotations]
|
|
543
|
+
).timestamp()
|
|
544
|
+
end_time = max(
|
|
545
|
+
[rotation.trips[-1].arrival_time for rotation in rotations]
|
|
546
|
+
).timestamp()
|
|
547
|
+
timestamps_to_sample = np.arange(start_time, end_time, 60)
|
|
548
|
+
occupancy = np.zeros_like(timestamps_to_sample)
|
|
549
|
+
clean_occupancy = np.zeros_like(timestamps_to_sample)
|
|
550
|
+
for rotation in rotations:
|
|
551
|
+
rotation_start = rotation.trips[0].departure_time.timestamp()
|
|
552
|
+
rotation_end = rotation.trips[-1].arrival_time.timestamp()
|
|
553
|
+
occupancy += np.interp(
|
|
554
|
+
timestamps_to_sample,
|
|
555
|
+
[rotation_start, rotation_end],
|
|
556
|
+
[1, 1],
|
|
557
|
+
left=0,
|
|
558
|
+
right=0,
|
|
559
|
+
)
|
|
560
|
+
clean_occupancy += np.interp(
|
|
561
|
+
timestamps_to_sample,
|
|
562
|
+
[rotation_end, rotation_end + CLEAN_DURATION],
|
|
563
|
+
[1, 1],
|
|
564
|
+
left=0,
|
|
565
|
+
right=0,
|
|
566
|
+
)
|
|
567
|
+
max_occupancies[vehicle_type] = max(
|
|
568
|
+
max(occupancy), 1
|
|
569
|
+
) # To avoid zero occupancy
|
|
570
|
+
max_clean_occupancies[vehicle_type] = max(max(clean_occupancy), 1)
|
|
571
|
+
# Create a simple depot at this station
|
|
572
|
+
create_simple_depot(
|
|
573
|
+
scenario=scenario,
|
|
574
|
+
station=first_stop,
|
|
575
|
+
charging_capacities=max_occupancies,
|
|
576
|
+
cleaning_capacities=max_clean_occupancies,
|
|
577
|
+
charging_power=charging_power,
|
|
578
|
+
session=session,
|
|
579
|
+
cleaning_duration=timedelta(seconds=CLEAN_DURATION),
|
|
580
|
+
safety_margin=0.2,
|
|
581
|
+
shunting_duration=shunting_duration,
|
|
582
|
+
)
|
|
@@ -135,21 +135,9 @@ def optimize_charging_events_even(charging_events: List[Event]) -> None:
|
|
|
135
135
|
optimized_power, params_for_event["max_power"]
|
|
136
136
|
)
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
) # kWh
|
|
142
|
-
if energy_to_distribute > 0:
|
|
143
|
-
optimized_power_not_capped = np.where(
|
|
144
|
-
optimized_power > optimized_power_capped
|
|
145
|
-
)
|
|
146
|
-
# Distribute the energy over the time when the vehicle is not at peak power
|
|
147
|
-
uncapped_duration = (
|
|
148
|
-
optimized_power_not_capped[0].shape[0]
|
|
149
|
-
* TEMPORAL_RESOLUTION.total_seconds()
|
|
150
|
-
)
|
|
151
|
-
power_to_add = energy_to_distribute / (uncapped_duration / 3600)
|
|
152
|
-
optimized_power[optimized_power_not_capped[0]] += power_to_add
|
|
138
|
+
if not np.all(optimized_power_capped != optimized_power):
|
|
139
|
+
# If the power draw is capped, we will just use the mean power draw
|
|
140
|
+
optimized_power = params_for_event["charging_allowed"] * mean_power
|
|
153
141
|
|
|
154
142
|
# Make sure the transferred energy is the same
|
|
155
143
|
post_opt_energy = (
|
|
@@ -221,15 +209,10 @@ def optimize_charging_events_even(charging_events: List[Event]) -> None:
|
|
|
221
209
|
params_for_event["transferred_energy"] / post_opt_energy
|
|
222
210
|
)
|
|
223
211
|
|
|
224
|
-
optimized_power2 = (
|
|
225
|
-
params_for_event["charging_allowed"] * mean_power
|
|
226
|
-
) # * (mean_occupancy / total_occupancy)
|
|
227
212
|
# Fill NaNs with the zero power draw
|
|
228
213
|
optimized_power[np.isnan(optimized_power)] = 0
|
|
229
|
-
optimized_power2[np.isnan(optimized_power2)] = 0
|
|
230
214
|
|
|
231
215
|
params_for_event["optimized_power"] = optimized_power
|
|
232
|
-
params_for_event["optimized_power2"] = optimized_power2
|
|
233
216
|
|
|
234
217
|
event = params_for_event["event"]
|
|
235
218
|
start_index = int((event.time_start - start_time) / TEMPORAL_RESOLUTION)
|
eflips/depot/api/private/util.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""This module contains miscellaneous utility functions for the eflips-depot API."""
|
|
2
|
-
|
|
2
|
+
import logging
|
|
3
3
|
import os
|
|
4
4
|
from contextlib import contextmanager
|
|
5
5
|
from dataclasses import dataclass
|
|
@@ -40,6 +40,8 @@ def create_session(
|
|
|
40
40
|
database, or any other object that has an attribute `id` that is an integer.
|
|
41
41
|
:return: Yield a Tuple of the session and the scenario.
|
|
42
42
|
"""
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
43
45
|
managed_session = False
|
|
44
46
|
engine = None
|
|
45
47
|
session = None
|
|
@@ -47,6 +49,10 @@ def create_session(
|
|
|
47
49
|
if isinstance(scenario, Scenario):
|
|
48
50
|
session = inspect(scenario).session
|
|
49
51
|
elif isinstance(scenario, int) or hasattr(scenario, "id"):
|
|
52
|
+
logger.warning(
|
|
53
|
+
"Scenario passed was not part of an active session. Uncommited changes will be ignored."
|
|
54
|
+
)
|
|
55
|
+
|
|
50
56
|
if isinstance(scenario, int):
|
|
51
57
|
scenario_id = scenario
|
|
52
58
|
else:
|
|
@@ -348,8 +354,16 @@ class VehicleSchedule:
|
|
|
348
354
|
opportunity_charging = rot.allow_opportunity_charging
|
|
349
355
|
|
|
350
356
|
# Find the depot at the start and end of the rotation
|
|
351
|
-
start_depot =
|
|
352
|
-
|
|
357
|
+
start_depot = (
|
|
358
|
+
session.query(Depot)
|
|
359
|
+
.filter(Depot.station_id == trips[0].route.departure_station_id)
|
|
360
|
+
.one()
|
|
361
|
+
)
|
|
362
|
+
end_depot = (
|
|
363
|
+
session.query(Depot)
|
|
364
|
+
.filter(Depot.station_id == trips[-1].route.arrival_station_id)
|
|
365
|
+
.one()
|
|
366
|
+
)
|
|
353
367
|
|
|
354
368
|
if start_depot is None or end_depot is None:
|
|
355
369
|
raise ValueError(f"Rotation {rot.id} has no depot at the start or end.")
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
eflips/depot/__init__.py,sha256=n7jte8R6j_Ad4Mp4hkklKwil5r8u8Q_SbXrCC-nf5jM,1556
|
|
2
|
-
eflips/depot/api/__init__.py,sha256=
|
|
2
|
+
eflips/depot/api/__init__.py,sha256=NInu2K-TmKlJSpBNXa1boMxmSM2odxLN4qwjgMjTjhE,93971
|
|
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/depot.py,sha256=
|
|
6
|
-
eflips/depot/api/private/smart_charging.py,sha256=
|
|
7
|
-
eflips/depot/api/private/util.py,sha256=
|
|
5
|
+
eflips/depot/api/private/depot.py,sha256=IpXQR2GuEUuilP02njKpvExgSkktkyHPmcn2j_Q3YPk,21276
|
|
6
|
+
eflips/depot/api/private/smart_charging.py,sha256=LcD0SN26O6KPBL5Ha7tpovnMn_4dAi17Kb21jEeAoE0,13052
|
|
7
|
+
eflips/depot/api/private/util.py,sha256=Ye-WXNzHcNfunFijK7FCIU3AiCuMg83KnEhnKbtlZu8,17242
|
|
8
8
|
eflips/depot/configuration.py,sha256=Op3hlir-dEN7yHr0kTqbYANoCBKFWK6uKOv3NJl8w_w,35678
|
|
9
9
|
eflips/depot/depot.py,sha256=afIlaiX-J-M5-K_oAGMr_soL3_QjIAwrQKDaZzTwle0,105566
|
|
10
10
|
eflips/depot/evaluation.py,sha256=qqXyP4jA1zFcKuWhliQ6n25ZlGl9mJV-vtXf0yu8WN8,140842
|
|
@@ -35,7 +35,7 @@ eflips/depot/simulation.py,sha256=ee0qTzOzG-8ybN36ie_NJallXfC7jUaS9JZvaYFziLs,10
|
|
|
35
35
|
eflips/depot/smart_charging.py,sha256=C3BYqzn2-OYY4ipXm0ETtavbAM9QXZMYULBpVoChf0E,54311
|
|
36
36
|
eflips/depot/standalone.py,sha256=VxcTzBaB67fNJUMmjPRwKXjhqTy6oQ41Coote2LvAmk,22338
|
|
37
37
|
eflips/depot/validation.py,sha256=TIuY7cQtEJI4H2VVMSuY5IIVkacEEZ67weeMuY3NSAM,7097
|
|
38
|
-
eflips_depot-4.3.
|
|
39
|
-
eflips_depot-4.3.
|
|
40
|
-
eflips_depot-4.3.
|
|
41
|
-
eflips_depot-4.3.
|
|
38
|
+
eflips_depot-4.3.5.dist-info/LICENSE.md,sha256=KB4XTk1fPHjtZCYDyPyreu6h1LVJVZXYg-5vePcWZAc,34143
|
|
39
|
+
eflips_depot-4.3.5.dist-info/METADATA,sha256=VKdBOsoxsqEnPTu7HG1Kde2nrlGi9TW4Aln1fPx3mE0,5839
|
|
40
|
+
eflips_depot-4.3.5.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
41
|
+
eflips_depot-4.3.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|