eflips-depot 4.3.19__py3-none-any.whl → 4.4.0__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 -304
- eflips/depot/api/private/depot.py +616 -524
- eflips/depot/api/private/results_to_database.py +2 -1
- eflips/depot/api/private/smart_charging.py +7 -6
- eflips/depot/api/private/util.py +3 -20
- {eflips_depot-4.3.19.dist-info → eflips_depot-4.4.0.dist-info}/METADATA +2 -1
- {eflips_depot-4.3.19.dist-info → eflips_depot-4.4.0.dist-info}/RECORD +9 -9
- {eflips_depot-4.3.19.dist-info → eflips_depot-4.4.0.dist-info}/LICENSE.md +0 -0
- {eflips_depot-4.3.19.dist-info → eflips_depot-4.4.0.dist-info}/WHEEL +0 -0
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
"""This package contains the private API for the depot-related functionality in eFLIPS."""
|
|
2
|
-
import itertools
|
|
3
2
|
import logging
|
|
3
|
+
import math
|
|
4
4
|
from datetime import timedelta
|
|
5
5
|
from enum import Enum, auto
|
|
6
|
-
from math import ceil
|
|
7
6
|
from typing import Dict, List, Tuple
|
|
8
7
|
|
|
9
|
-
import eflips.model
|
|
10
8
|
import numpy as np
|
|
11
9
|
import sqlalchemy.orm
|
|
12
10
|
from eflips.model import (
|
|
@@ -23,12 +21,12 @@ from eflips.model import (
|
|
|
23
21
|
Trip,
|
|
24
22
|
Station,
|
|
25
23
|
VehicleType,
|
|
24
|
+
Vehicle,
|
|
25
|
+
EventType,
|
|
26
26
|
)
|
|
27
|
+
from sqlalchemy import or_
|
|
27
28
|
from sqlalchemy.orm import Session
|
|
28
29
|
|
|
29
|
-
from eflips.depot import DepotEvaluation, LineArea, DirectArea
|
|
30
|
-
from eflips.depot.evaluation import to_prev_values
|
|
31
|
-
|
|
32
30
|
|
|
33
31
|
def delete_depots(scenario: Scenario, session: Session) -> None:
|
|
34
32
|
"""This function deletes all depot-related data from the database for a given scenario.
|
|
@@ -91,10 +89,6 @@ def depot_to_template(depot: Depot) -> Dict[str, str | Dict[str, str | int]]:
|
|
|
91
89
|
# Helper for adding processes to the template
|
|
92
90
|
list_of_processes = []
|
|
93
91
|
|
|
94
|
-
# Load all areas, sorted by their type
|
|
95
|
-
area_type_order = [AreaType.LINE, AreaType.DIRECT_ONESIDE, AreaType.DIRECT_TWOSIDE]
|
|
96
|
-
sorted_areas = sorted(depot.areas, key=lambda x: area_type_order.index(x.area_type))
|
|
97
|
-
|
|
98
92
|
# Get dictionary of each area
|
|
99
93
|
for area in depot.areas:
|
|
100
94
|
area_name = str(area.id)
|
|
@@ -302,171 +296,6 @@ def group_rotations_by_start_end_stop(
|
|
|
302
296
|
return grouped_rotations
|
|
303
297
|
|
|
304
298
|
|
|
305
|
-
def create_simple_depot(
|
|
306
|
-
scenario: Scenario,
|
|
307
|
-
station: Station,
|
|
308
|
-
charging_capacities: Dict[VehicleType, int],
|
|
309
|
-
cleaning_capacities: Dict[VehicleType, int],
|
|
310
|
-
charging_power: float,
|
|
311
|
-
session: sqlalchemy.orm.session.Session,
|
|
312
|
-
cleaning_duration: timedelta = timedelta(minutes=30),
|
|
313
|
-
safety_margin: float = 0.0,
|
|
314
|
-
shunting_duration: timedelta = timedelta(minutes=5),
|
|
315
|
-
) -> None:
|
|
316
|
-
"""
|
|
317
|
-
Creates a simple depot for a given scenario.
|
|
318
|
-
|
|
319
|
-
It has one area for each vehicle type and a charging process for each
|
|
320
|
-
area. Also an arrival area for each vehicle type.
|
|
321
|
-
|
|
322
|
-
:param safety_margin: a safety margin for the number of charging and cleaning capacities. Default is 0.0
|
|
323
|
-
:param scenario: The scenario to be simulated
|
|
324
|
-
:param station: The station where the depot is located
|
|
325
|
-
:param charging_capacities: A dictionary of vehicle types and the number of vehicles that can be charged at the same time
|
|
326
|
-
:param cleaning_capacities: A dictionary of vehicle types and the number of vehicles that can be cleaned at the same time
|
|
327
|
-
:param charging_power: The power of the charging process
|
|
328
|
-
:param cleaning_duration: The duration of the cleaning process
|
|
329
|
-
:param session: An SQLAlchemy session object to the database
|
|
330
|
-
:return: Nothing. Depots are created in the database.
|
|
331
|
-
"""
|
|
332
|
-
|
|
333
|
-
# Create a simple depot
|
|
334
|
-
depot = Depot(
|
|
335
|
-
scenario=scenario,
|
|
336
|
-
name=f"Depot at {station.name}",
|
|
337
|
-
name_short=station.name_short,
|
|
338
|
-
station_id=station.id,
|
|
339
|
-
)
|
|
340
|
-
session.add(depot)
|
|
341
|
-
|
|
342
|
-
# Create plan
|
|
343
|
-
plan = Plan(scenario=scenario, name=f"Default Plan")
|
|
344
|
-
session.add(plan)
|
|
345
|
-
|
|
346
|
-
depot.default_plan = plan
|
|
347
|
-
|
|
348
|
-
# Create processes
|
|
349
|
-
shunting_1 = Process(
|
|
350
|
-
name="Shunting 1",
|
|
351
|
-
scenario=scenario,
|
|
352
|
-
dispatchable=False,
|
|
353
|
-
duration=shunting_duration,
|
|
354
|
-
)
|
|
355
|
-
clean = Process(
|
|
356
|
-
name="Arrival Cleaning",
|
|
357
|
-
scenario=scenario,
|
|
358
|
-
dispatchable=False,
|
|
359
|
-
duration=cleaning_duration,
|
|
360
|
-
)
|
|
361
|
-
shunting_2 = Process(
|
|
362
|
-
name="Shunting 2",
|
|
363
|
-
scenario=scenario,
|
|
364
|
-
dispatchable=False,
|
|
365
|
-
duration=shunting_duration,
|
|
366
|
-
)
|
|
367
|
-
charging = Process(
|
|
368
|
-
name="Charging",
|
|
369
|
-
scenario=scenario,
|
|
370
|
-
dispatchable=True,
|
|
371
|
-
electric_power=charging_power,
|
|
372
|
-
)
|
|
373
|
-
standby_departure = Process(
|
|
374
|
-
name="Standby Pre-departure",
|
|
375
|
-
scenario=scenario,
|
|
376
|
-
dispatchable=True,
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
session.add(clean)
|
|
380
|
-
session.add(shunting_1)
|
|
381
|
-
session.add(charging)
|
|
382
|
-
session.add(standby_departure)
|
|
383
|
-
session.add(shunting_2)
|
|
384
|
-
|
|
385
|
-
# Create shared waiting area
|
|
386
|
-
waiting_area = Area(
|
|
387
|
-
scenario=scenario,
|
|
388
|
-
name=f"Waiting Area for every type of vehicle",
|
|
389
|
-
depot=depot,
|
|
390
|
-
area_type=AreaType.DIRECT_ONESIDE,
|
|
391
|
-
capacity=100,
|
|
392
|
-
)
|
|
393
|
-
session.add(waiting_area)
|
|
394
|
-
|
|
395
|
-
for vehicle_type in charging_capacities.keys():
|
|
396
|
-
charging_count = charging_capacities[vehicle_type]
|
|
397
|
-
|
|
398
|
-
charging_count = int(ceil(charging_count * (1 + safety_margin)))
|
|
399
|
-
|
|
400
|
-
# Create charging area
|
|
401
|
-
charging_area = Area(
|
|
402
|
-
scenario=scenario,
|
|
403
|
-
name=f"Direct Charging Area for {vehicle_type.name_short}",
|
|
404
|
-
depot=depot,
|
|
405
|
-
area_type=AreaType.DIRECT_ONESIDE,
|
|
406
|
-
capacity=int(charging_count * 1),
|
|
407
|
-
)
|
|
408
|
-
session.add(charging_area)
|
|
409
|
-
charging_area.vehicle_type = vehicle_type
|
|
410
|
-
|
|
411
|
-
# Create cleaning area
|
|
412
|
-
cleaning_count = cleaning_capacities[vehicle_type]
|
|
413
|
-
|
|
414
|
-
cleaning_count = int(ceil(cleaning_count * (1 + safety_margin)))
|
|
415
|
-
|
|
416
|
-
cleaning_area = Area(
|
|
417
|
-
scenario=scenario,
|
|
418
|
-
name=f"Cleaning Area for {vehicle_type.name_short}",
|
|
419
|
-
depot=depot,
|
|
420
|
-
area_type=AreaType.DIRECT_ONESIDE,
|
|
421
|
-
capacity=cleaning_count,
|
|
422
|
-
)
|
|
423
|
-
session.add(cleaning_area)
|
|
424
|
-
cleaning_area.vehicle_type = vehicle_type
|
|
425
|
-
|
|
426
|
-
shunting_area_1 = Area(
|
|
427
|
-
scenario=scenario,
|
|
428
|
-
name=f"Shunting Area 1 for {vehicle_type.name_short}",
|
|
429
|
-
depot=depot,
|
|
430
|
-
area_type=AreaType.DIRECT_ONESIDE,
|
|
431
|
-
capacity=10,
|
|
432
|
-
)
|
|
433
|
-
|
|
434
|
-
session.add(shunting_area_1)
|
|
435
|
-
shunting_area_1.vehicle_type = vehicle_type
|
|
436
|
-
|
|
437
|
-
shunting_area_2 = Area(
|
|
438
|
-
scenario=scenario,
|
|
439
|
-
name=f"Shunting Area 2 for {vehicle_type.name_short}",
|
|
440
|
-
depot=depot,
|
|
441
|
-
area_type=AreaType.DIRECT_ONESIDE,
|
|
442
|
-
capacity=10,
|
|
443
|
-
)
|
|
444
|
-
|
|
445
|
-
session.add(shunting_area_2)
|
|
446
|
-
shunting_area_2.vehicle_type = vehicle_type
|
|
447
|
-
|
|
448
|
-
cleaning_area.processes.append(clean)
|
|
449
|
-
charging_area.processes.append(charging)
|
|
450
|
-
charging_area.processes.append(standby_departure)
|
|
451
|
-
shunting_area_1.processes.append(shunting_1)
|
|
452
|
-
shunting_area_2.processes.append(shunting_2)
|
|
453
|
-
|
|
454
|
-
assocs = [
|
|
455
|
-
AssocPlanProcess(
|
|
456
|
-
scenario=scenario, process=shunting_1, plan=plan, ordinal=0
|
|
457
|
-
),
|
|
458
|
-
AssocPlanProcess(scenario=scenario, process=clean, plan=plan, ordinal=1),
|
|
459
|
-
AssocPlanProcess(
|
|
460
|
-
scenario=scenario, process=shunting_2, plan=plan, ordinal=2
|
|
461
|
-
),
|
|
462
|
-
AssocPlanProcess(scenario=scenario, process=charging, plan=plan, ordinal=3),
|
|
463
|
-
AssocPlanProcess(
|
|
464
|
-
scenario=scenario, process=standby_departure, plan=plan, ordinal=4
|
|
465
|
-
),
|
|
466
|
-
]
|
|
467
|
-
session.add_all(assocs)
|
|
468
|
-
|
|
469
|
-
|
|
470
299
|
class ProcessType(Enum):
|
|
471
300
|
"""This class represents the types of a process in eFLIPS-Depot."""
|
|
472
301
|
|
|
@@ -521,157 +350,66 @@ def process_type(p: Process) -> ProcessType:
|
|
|
521
350
|
raise ValueError("Invalid process type")
|
|
522
351
|
|
|
523
352
|
|
|
524
|
-
def
|
|
525
|
-
|
|
526
|
-
charging_power: float,
|
|
527
|
-
first_stop: Station,
|
|
528
|
-
scenario: Scenario,
|
|
529
|
-
session: sqlalchemy.orm.session.Session,
|
|
530
|
-
vehicle_type_dict: Dict[VehicleType, List[Rotation]],
|
|
531
|
-
shunting_duration: timedelta = timedelta(minutes=5),
|
|
532
|
-
) -> None:
|
|
533
|
-
"""
|
|
534
|
-
Private inner function to generate a depot layout with an arrival and a charging area for each vehicle type.
|
|
535
|
-
|
|
536
|
-
:param CLEAN_DURATION: The duration of the cleaning process in seconds.
|
|
537
|
-
:param charging_power: The charging power of the charging area in kW.
|
|
538
|
-
:param first_stop: The stop where the depot is located.
|
|
539
|
-
:param scenario: The scenario for which the depot layout should be generated.
|
|
540
|
-
:param session: The SQLAlchemy session object.
|
|
541
|
-
:param vehicle_type_dict: A dictionary with vehicle types as keys and rotations as values.
|
|
542
|
-
:return: Nothing. The depot layout is created in the database.
|
|
543
|
-
"""
|
|
544
|
-
max_occupancies: Dict[eflips.model.VehicleType, int] = {}
|
|
545
|
-
max_clean_occupancies: Dict[eflips.model.VehicleType, int] = {}
|
|
546
|
-
for vehicle_type, rotations in vehicle_type_dict.items():
|
|
547
|
-
# Slightly convoluted vehicle summation
|
|
548
|
-
start_time = min(
|
|
549
|
-
[rotation.trips[0].departure_time for rotation in rotations]
|
|
550
|
-
).timestamp()
|
|
551
|
-
end_time = max(
|
|
552
|
-
[rotation.trips[-1].arrival_time for rotation in rotations]
|
|
553
|
-
).timestamp()
|
|
554
|
-
timestamps_to_sample = np.arange(start_time, end_time, 60)
|
|
555
|
-
occupancy = np.zeros_like(timestamps_to_sample)
|
|
556
|
-
clean_occupancy = np.zeros_like(timestamps_to_sample)
|
|
557
|
-
for rotation in rotations:
|
|
558
|
-
rotation_start = rotation.trips[0].departure_time.timestamp()
|
|
559
|
-
rotation_end = rotation.trips[-1].arrival_time.timestamp()
|
|
560
|
-
occupancy += np.interp(
|
|
561
|
-
timestamps_to_sample,
|
|
562
|
-
[rotation_start, rotation_end],
|
|
563
|
-
[1, 1],
|
|
564
|
-
left=0,
|
|
565
|
-
right=0,
|
|
566
|
-
)
|
|
567
|
-
clean_occupancy += np.interp(
|
|
568
|
-
timestamps_to_sample,
|
|
569
|
-
[rotation_end, rotation_end + CLEAN_DURATION],
|
|
570
|
-
[1, 1],
|
|
571
|
-
left=0,
|
|
572
|
-
right=0,
|
|
573
|
-
)
|
|
574
|
-
max_occupancies[vehicle_type] = max(
|
|
575
|
-
max(occupancy), 1
|
|
576
|
-
) # To avoid zero occupancy
|
|
577
|
-
max_clean_occupancies[vehicle_type] = max(max(clean_occupancy), 1)
|
|
578
|
-
# Create a simple depot at this station
|
|
579
|
-
create_simple_depot(
|
|
580
|
-
scenario=scenario,
|
|
581
|
-
station=first_stop,
|
|
582
|
-
charging_capacities=max_occupancies,
|
|
583
|
-
cleaning_capacities=max_clean_occupancies,
|
|
584
|
-
charging_power=charging_power,
|
|
585
|
-
session=session,
|
|
586
|
-
cleaning_duration=timedelta(seconds=CLEAN_DURATION),
|
|
587
|
-
safety_margin=0.2,
|
|
588
|
-
shunting_duration=shunting_duration,
|
|
589
|
-
)
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
def generate_line_depot_layout(
|
|
593
|
-
CLEAN_DURATION: int,
|
|
594
|
-
charging_power: float,
|
|
353
|
+
def generate_depot(
|
|
354
|
+
capacity_of_areas: Dict[VehicleType, Dict[AreaType, None | int]],
|
|
595
355
|
station: Station,
|
|
596
356
|
scenario: Scenario,
|
|
597
357
|
session: sqlalchemy.orm.session.Session,
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
358
|
+
standard_block_length: int = 6,
|
|
359
|
+
shunting_duration: None | timedelta = timedelta(minutes=5),
|
|
360
|
+
num_shunting_slots: int = 10,
|
|
361
|
+
cleaning_duration: None | timedelta = timedelta(minutes=30),
|
|
362
|
+
num_cleaning_slots: int = 10,
|
|
363
|
+
charging_power: float = 90,
|
|
603
364
|
) -> None:
|
|
604
365
|
"""
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
:param
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
:param
|
|
612
|
-
:param
|
|
613
|
-
|
|
614
|
-
:param
|
|
615
|
-
:param
|
|
616
|
-
:param
|
|
617
|
-
:
|
|
366
|
+
Creates a depot object with all associated data structures and adds them to the database.
|
|
367
|
+
|
|
368
|
+
:param capacity_of_areas: A dictionary of vehicle types and the number of areas for each type.
|
|
369
|
+
Example: {VehicleType<"Electric Bus">: {AreaType.LINE: 3, AreaType.DIRECT_ONESIDE: 2}}
|
|
370
|
+
For no areas of a certain type, set the value to None or zero. An exception will be raised if a LINE
|
|
371
|
+
area's capacity is not a multiple of the standard block length.
|
|
372
|
+
:param station: The station where the depot is located.
|
|
373
|
+
:param scenario: The scenario to be simulated. An Exception will be raised if this differs from the station's scenario.
|
|
374
|
+
:param session: An open SQLAlchemy session.
|
|
375
|
+
:param standard_block_length: The block length (number of vehicles behind each other) for LINE areas. Defaults to 6.
|
|
376
|
+
:param shunting_duration: The duration of the shunting process. Defaults to 5 minutes. Set to None if not needed.
|
|
377
|
+
:param num_shunting_slots: The number of slots for shunting. Defaults to 10.
|
|
378
|
+
:param cleaning_duration: The duration of the cleaning process. Defaults to 30 minutes. Set to None if not needed.
|
|
379
|
+
:param num_cleaning_slots: The number of slots for cleaning. Defaults to 10.
|
|
380
|
+
:param charging_power: The charging power in kW. Defaults to 90 kW.
|
|
381
|
+
:return: Nothing. Depot is added to the database.
|
|
618
382
|
"""
|
|
619
|
-
logger = logging.getLogger(__name__)
|
|
620
|
-
DEBUG_PLOT = False
|
|
621
|
-
|
|
622
|
-
# In order to figure out how many cleaning areas we need, we look at the number of vehicle simultaneously being
|
|
623
|
-
# cleaned. This is the number of vehicles simulatenously being within the "CLEAN_DURATION" after their arrival.
|
|
624
|
-
|
|
625
|
-
# We assemble a vector of all time in the simulation
|
|
626
|
-
logger.info("Calculating the number of cleaning areas needed")
|
|
627
|
-
all_rotations = list(itertools.chain(*vehicle_type_rotation_dict.values()))
|
|
628
|
-
start_time = min(
|
|
629
|
-
[rotation.trips[0].departure_time for rotation in all_rotations]
|
|
630
|
-
).timestamp()
|
|
631
|
-
end_time = max(
|
|
632
|
-
[rotation.trips[-1].arrival_time for rotation in all_rotations]
|
|
633
|
-
).timestamp()
|
|
634
|
-
timestamps_to_sample = np.arange(start_time, end_time, 60)
|
|
635
|
-
clean_occupancy = np.zeros_like(timestamps_to_sample)
|
|
636
|
-
|
|
637
|
-
# Then fir each arrival, we add 1 to the CLEAN_DURATION after the arrival
|
|
638
|
-
for rotation in all_rotations:
|
|
639
|
-
rotation_end = rotation.trips[-1].arrival_time.timestamp()
|
|
640
|
-
clean_occupancy += np.interp(
|
|
641
|
-
timestamps_to_sample,
|
|
642
|
-
[rotation_end, rotation_end + CLEAN_DURATION],
|
|
643
|
-
[1, 1],
|
|
644
|
-
left=0,
|
|
645
|
-
right=0,
|
|
646
|
-
)
|
|
647
383
|
|
|
648
|
-
|
|
649
|
-
|
|
384
|
+
# Sanity checks
|
|
385
|
+
# Make sure the capacity of areas is valid.
|
|
386
|
+
for key, value in capacity_of_areas.items():
|
|
387
|
+
key: VehicleType
|
|
388
|
+
value: Dict[AreaType, None | int]
|
|
389
|
+
for possible_area_type in AreaType:
|
|
390
|
+
if possible_area_type not in value:
|
|
391
|
+
value[possible_area_type] = None
|
|
392
|
+
if (
|
|
393
|
+
value[AreaType.LINE] is not None
|
|
394
|
+
and value[AreaType.LINE] % standard_block_length != 0
|
|
395
|
+
):
|
|
396
|
+
raise ValueError(
|
|
397
|
+
f"LINE area capacity for {key.name} is not a multiple of the standard block length."
|
|
398
|
+
)
|
|
650
399
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
400
|
+
if (
|
|
401
|
+
value[AreaType.DIRECT_TWOSIDE] is not None
|
|
402
|
+
and value[AreaType.DIRECT_TWOSIDE] % 2 != 0
|
|
403
|
+
):
|
|
404
|
+
raise ValueError(
|
|
405
|
+
f"DIRECT_TWOSIDE area capacity for {key.name} is not a multiple of 2."
|
|
406
|
+
)
|
|
654
407
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
)
|
|
408
|
+
# Make sure the scenario is the same as the station's scenario
|
|
409
|
+
if station.scenario_id != scenario.id:
|
|
410
|
+
raise ValueError("The scenario and station do not match.")
|
|
659
411
|
|
|
660
|
-
#
|
|
661
|
-
clean_areas_needed = ceil(vehicles_arriving_in_window / 2)
|
|
662
|
-
logger.info(f"Number of cleaning areas created: {clean_areas_needed}")
|
|
663
|
-
del all_rotations, clean_occupancy, timestamps_to_sample, start_time, end_time
|
|
664
|
-
|
|
665
|
-
# Create the depot
|
|
666
|
-
# `vehicles_arriving_in_window`+1 will be the size of our shunting areas
|
|
667
|
-
# `clean_areas_needed` will be the size of our cleaning areas
|
|
668
|
-
# We will create line and direct areas for each vehicle type
|
|
669
|
-
# - THe line areas will be of length `line_length` and count `line_counts[vehicle_type]`
|
|
670
|
-
# - The direct areas will be of length 1 and count `direct_counts[vehicle_type]`
|
|
671
|
-
# - The charging power for the line areas will be `charging_power_direct` and for the direct areas `charging_power`
|
|
672
|
-
# unless `charging_power_direct` is not set, in which case `charging_power` will be used.
|
|
673
|
-
|
|
674
|
-
# Create the depot
|
|
412
|
+
# Create a simple depot
|
|
675
413
|
depot = Depot(
|
|
676
414
|
scenario=scenario,
|
|
677
415
|
name=f"Depot at {station.name}",
|
|
@@ -680,29 +418,87 @@ def generate_line_depot_layout(
|
|
|
680
418
|
)
|
|
681
419
|
session.add(depot)
|
|
682
420
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
dispatchable=False,
|
|
687
|
-
duration=shunting_duration,
|
|
688
|
-
)
|
|
421
|
+
# Create plan
|
|
422
|
+
plan = Plan(scenario=scenario, name=f"Default Plan")
|
|
423
|
+
session.add(plan)
|
|
689
424
|
|
|
690
|
-
|
|
691
|
-
clean = Process(
|
|
692
|
-
name="Arrival Cleaning",
|
|
693
|
-
scenario=scenario,
|
|
694
|
-
dispatchable=False,
|
|
695
|
-
duration=timedelta(seconds=CLEAN_DURATION),
|
|
696
|
-
)
|
|
697
|
-
session.add(clean)
|
|
425
|
+
depot.default_plan = plan
|
|
698
426
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
427
|
+
assocs: List[AssocPlanProcess] = []
|
|
428
|
+
|
|
429
|
+
# Create processes
|
|
430
|
+
if shunting_duration is not None:
|
|
431
|
+
# Create processes
|
|
432
|
+
shunting_1 = Process(
|
|
433
|
+
name="Shunting 1",
|
|
434
|
+
scenario=scenario,
|
|
435
|
+
dispatchable=False,
|
|
436
|
+
duration=shunting_duration,
|
|
437
|
+
)
|
|
438
|
+
session.add(shunting_1)
|
|
439
|
+
shunting_area_1 = Area(
|
|
440
|
+
scenario=scenario,
|
|
441
|
+
name=f"Shunting Area 1",
|
|
442
|
+
depot=depot,
|
|
443
|
+
area_type=AreaType.DIRECT_ONESIDE,
|
|
444
|
+
vehicle_type=None, # Meaning any vehicle type can be shunted here
|
|
445
|
+
capacity=num_shunting_slots,
|
|
446
|
+
)
|
|
447
|
+
session.add(shunting_area_1)
|
|
448
|
+
shunting_area_1.processes.append(shunting_1)
|
|
449
|
+
assocs.append(
|
|
450
|
+
AssocPlanProcess(
|
|
451
|
+
scenario=scenario, process=shunting_1, plan=plan, ordinal=len(assocs)
|
|
452
|
+
)
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
if cleaning_duration is not None:
|
|
456
|
+
clean = Process(
|
|
457
|
+
name="Arrival Cleaning",
|
|
458
|
+
scenario=scenario,
|
|
459
|
+
dispatchable=False,
|
|
460
|
+
duration=cleaning_duration,
|
|
461
|
+
)
|
|
462
|
+
session.add(clean)
|
|
463
|
+
cleaning_area = Area(
|
|
464
|
+
scenario=scenario,
|
|
465
|
+
name=f"Cleaning Area",
|
|
466
|
+
depot=depot,
|
|
467
|
+
area_type=AreaType.DIRECT_ONESIDE,
|
|
468
|
+
vehicle_type=None, # Meaning any vehicle type can be cleaned here
|
|
469
|
+
capacity=num_cleaning_slots,
|
|
470
|
+
)
|
|
471
|
+
session.add(cleaning_area)
|
|
472
|
+
cleaning_area.processes.append(clean)
|
|
473
|
+
assocs.append(
|
|
474
|
+
AssocPlanProcess(
|
|
475
|
+
scenario=scenario, process=clean, plan=plan, ordinal=len(assocs)
|
|
476
|
+
)
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
if shunting_duration is not None:
|
|
480
|
+
shunting_2 = Process(
|
|
481
|
+
name="Shunting 2",
|
|
482
|
+
scenario=scenario,
|
|
483
|
+
dispatchable=False,
|
|
484
|
+
duration=shunting_duration,
|
|
485
|
+
)
|
|
486
|
+
session.add(shunting_2)
|
|
487
|
+
shunting_area_2 = Area(
|
|
488
|
+
scenario=scenario,
|
|
489
|
+
name=f"Shunting Area 2",
|
|
490
|
+
depot=depot,
|
|
491
|
+
area_type=AreaType.DIRECT_ONESIDE,
|
|
492
|
+
vehicle_type=None, # Meaning any vehicle type can be shunted here
|
|
493
|
+
capacity=num_shunting_slots,
|
|
494
|
+
)
|
|
495
|
+
session.add(shunting_area_2)
|
|
496
|
+
shunting_area_2.processes.append(shunting_2)
|
|
497
|
+
assocs.append(
|
|
498
|
+
AssocPlanProcess(
|
|
499
|
+
scenario=scenario, process=shunting_2, plan=plan, ordinal=len(assocs)
|
|
500
|
+
)
|
|
501
|
+
)
|
|
706
502
|
|
|
707
503
|
charging = Process(
|
|
708
504
|
name="Charging",
|
|
@@ -710,6 +506,12 @@ def generate_line_depot_layout(
|
|
|
710
506
|
dispatchable=True,
|
|
711
507
|
electric_power=charging_power,
|
|
712
508
|
)
|
|
509
|
+
session.add(charging)
|
|
510
|
+
assocs.append(
|
|
511
|
+
AssocPlanProcess(
|
|
512
|
+
scenario=scenario, process=charging, plan=plan, ordinal=len(assocs)
|
|
513
|
+
)
|
|
514
|
+
)
|
|
713
515
|
|
|
714
516
|
standby_departure = Process(
|
|
715
517
|
name="Standby Pre-departure",
|
|
@@ -717,9 +519,14 @@ def generate_line_depot_layout(
|
|
|
717
519
|
dispatchable=True,
|
|
718
520
|
)
|
|
719
521
|
session.add(standby_departure)
|
|
522
|
+
assocs.append(
|
|
523
|
+
AssocPlanProcess(
|
|
524
|
+
scenario=scenario, process=standby_departure, plan=plan, ordinal=len(assocs)
|
|
525
|
+
)
|
|
526
|
+
)
|
|
527
|
+
session.add_all(assocs) # It's complete, so add all at once
|
|
720
528
|
|
|
721
529
|
# Create shared waiting area
|
|
722
|
-
# This will be the "virtual" area where vehicles wait for a spot in the depot
|
|
723
530
|
waiting_area = Area(
|
|
724
531
|
scenario=scenario,
|
|
725
532
|
name=f"Waiting Area for every type of vehicle",
|
|
@@ -729,216 +536,501 @@ def generate_line_depot_layout(
|
|
|
729
536
|
)
|
|
730
537
|
session.add(waiting_area)
|
|
731
538
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
# Create a shared shunting area
|
|
757
|
-
shunting_area_2 = Area(
|
|
758
|
-
scenario=scenario,
|
|
759
|
-
name=f"Shunting Area 2 (Cleaning -> Charging)",
|
|
760
|
-
depot=depot,
|
|
761
|
-
area_type=AreaType.DIRECT_ONESIDE,
|
|
762
|
-
capacity=clean_areas_needed,
|
|
763
|
-
)
|
|
764
|
-
session.add(shunting_area_2)
|
|
765
|
-
shunting_area_2.processes.append(shunting_2)
|
|
766
|
-
|
|
767
|
-
# Create the line areas for each vehicle type
|
|
768
|
-
for vehicle_type, count in line_counts.items():
|
|
769
|
-
for i in range(count):
|
|
770
|
-
line_area = Area(
|
|
539
|
+
for vehicle_type, capacities in capacity_of_areas.items():
|
|
540
|
+
vehicle_type: VehicleType
|
|
541
|
+
capacities: Dict[AreaType, None | int]
|
|
542
|
+
if capacities[AreaType.LINE] is not None and capacities[AreaType.LINE] > 0:
|
|
543
|
+
# Create a number of LINE areas
|
|
544
|
+
number_of_areas = capacities[AreaType.LINE] // standard_block_length
|
|
545
|
+
for i in range(number_of_areas):
|
|
546
|
+
area = Area(
|
|
547
|
+
scenario=scenario,
|
|
548
|
+
name=f"Line Area {i + 1} for {vehicle_type.name_short}",
|
|
549
|
+
depot=depot,
|
|
550
|
+
area_type=AreaType.LINE,
|
|
551
|
+
vehicle_type=vehicle_type,
|
|
552
|
+
capacity=standard_block_length,
|
|
553
|
+
)
|
|
554
|
+
area.processes.append(charging)
|
|
555
|
+
area.processes.append(standby_departure)
|
|
556
|
+
session.add(area)
|
|
557
|
+
if (
|
|
558
|
+
capacities[AreaType.DIRECT_ONESIDE] is not None
|
|
559
|
+
and capacities[AreaType.DIRECT_ONESIDE] > 0
|
|
560
|
+
):
|
|
561
|
+
# Create a single DIRECT_ONESIDE area with the correct capacity
|
|
562
|
+
area = Area(
|
|
771
563
|
scenario=scenario,
|
|
772
|
-
name=f"
|
|
564
|
+
name=f"Direct Area for {vehicle_type.name_short}",
|
|
773
565
|
depot=depot,
|
|
774
|
-
area_type=AreaType.
|
|
775
|
-
capacity=line_length,
|
|
566
|
+
area_type=AreaType.DIRECT_ONESIDE,
|
|
776
567
|
vehicle_type=vehicle_type,
|
|
568
|
+
capacity=capacities[AreaType.DIRECT_ONESIDE],
|
|
777
569
|
)
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
570
|
+
area.processes.append(charging)
|
|
571
|
+
area.processes.append(standby_departure)
|
|
572
|
+
session.add(area)
|
|
573
|
+
if (
|
|
574
|
+
capacities[AreaType.DIRECT_TWOSIDE] is not None
|
|
575
|
+
and capacities[AreaType.DIRECT_TWOSIDE] > 0
|
|
576
|
+
):
|
|
577
|
+
# Create a single DIRECT_TWOSIDE area with the correct capacity
|
|
578
|
+
area = Area(
|
|
786
579
|
scenario=scenario,
|
|
787
|
-
name=f"Direct Area for {vehicle_type.
|
|
580
|
+
name=f"Direct Area for {vehicle_type.name_short}",
|
|
788
581
|
depot=depot,
|
|
789
|
-
area_type=AreaType.
|
|
790
|
-
capacity=count,
|
|
582
|
+
area_type=AreaType.DIRECT_TWOSIDE,
|
|
791
583
|
vehicle_type=vehicle_type,
|
|
584
|
+
capacity=capacities[AreaType.DIRECT_TWOSIDE],
|
|
792
585
|
)
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
586
|
+
area.processes.append(charging)
|
|
587
|
+
area.processes.append(standby_departure)
|
|
588
|
+
session.add(area)
|
|
796
589
|
|
|
797
|
-
|
|
798
|
-
# Create plan
|
|
799
|
-
plan = Plan(scenario=scenario, name=f"Default Plan")
|
|
800
|
-
session.add(plan)
|
|
590
|
+
session.flush()
|
|
801
591
|
|
|
802
|
-
depot.default_plan = plan
|
|
803
592
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
session.add_all(assocs)
|
|
593
|
+
def area_needed_for_vehicle_parking(
|
|
594
|
+
vehicle_type: VehicleType,
|
|
595
|
+
count: int,
|
|
596
|
+
area_type: AreaType,
|
|
597
|
+
standard_block_length: int = 6,
|
|
598
|
+
spacing: float = 0.5,
|
|
599
|
+
angle=45,
|
|
600
|
+
) -> float:
|
|
601
|
+
"""
|
|
602
|
+
Calculates the area (in m²) needed to park a given number of vehicles of a given type.
|
|
815
603
|
|
|
604
|
+
DOes not take into account
|
|
605
|
+
the area needed to drive in and out of the parking spots.
|
|
816
606
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
607
|
+
- For AreaType.LINE, the vehicle count is rounded up to the next multiple of the standard block length.
|
|
608
|
+
- For AreaType.DIRECT_ONESIDE, the vehicle count is used as is.
|
|
609
|
+
- For AreaType.DIRECT_TWOSIDE, the vehicle count is rounded up to the next even number.
|
|
820
610
|
|
|
821
|
-
For the
|
|
611
|
+
For the DIRECT area types, an angle of 45° is assumed for the vehicles.
|
|
822
612
|
|
|
823
|
-
:param
|
|
824
|
-
:
|
|
613
|
+
:param vehicle_type: The vehicle type to calculate the area for.
|
|
614
|
+
:param count: The number of vehicles to park.
|
|
615
|
+
:param area_type: The type of the area to calculate the area for.
|
|
616
|
+
:param standard_block_length: The standard block length to use for LINE areas. Defaults to 6 (vehicles behind each other).
|
|
617
|
+
:param spacing: The space needed on the sides of the vehicles. Defaults to 0.5m.
|
|
618
|
+
:param angle: The angle the vehicles are parked at in direct areas, in degrees. Defaults to 45°. *Only used for direct areas.*
|
|
619
|
+
:return: The area needed in m².
|
|
825
620
|
"""
|
|
826
|
-
area_types_by_id: Dict[int, AreaType] = dict()
|
|
827
|
-
total_counts_by_area: Dict[str, Dict[str, np.ndarray]] = dict()
|
|
828
|
-
|
|
829
|
-
# We are assuming that the smulation runs for at least four days
|
|
830
|
-
SECONDS_IN_A_DAY = 24 * 60 * 60
|
|
831
|
-
assert ev.SIM_TIME >= 4 * SECONDS_IN_A_DAY
|
|
832
|
-
|
|
833
|
-
for area in ev.depot.list_areas:
|
|
834
|
-
# We need to figure out which kind of area this is
|
|
835
|
-
# We do this by looking at the vehicle type of the area
|
|
836
|
-
if len(area.entry_filter.filters) > 0:
|
|
837
|
-
if isinstance(area, LineArea):
|
|
838
|
-
area_types_by_id[area.ID] = AreaType.LINE
|
|
839
|
-
elif isinstance(area, DirectArea):
|
|
840
|
-
area_types_by_id[area.ID] = AreaType.DIRECT_ONESIDE
|
|
841
|
-
else:
|
|
842
|
-
raise ValueError("Unknown area type")
|
|
843
|
-
|
|
844
|
-
assert len(area.entry_filter.vehicle_types_str) == 1
|
|
845
|
-
vehicle_type_name = area.entry_filter.vehicle_types_str[0]
|
|
846
|
-
|
|
847
|
-
nv = area.logger.get_valList("count", SIM_TIME=ev.SIM_TIME)
|
|
848
|
-
nv = to_prev_values(nv)
|
|
849
|
-
nv = np.array(nv)
|
|
850
|
-
|
|
851
|
-
# If the area is empty, we don't care about it
|
|
852
|
-
if np.all(nv == 0):
|
|
853
|
-
continue
|
|
854
|
-
|
|
855
|
-
if vehicle_type_name not in total_counts_by_area:
|
|
856
|
-
total_counts_by_area[vehicle_type_name] = dict()
|
|
857
|
-
# We don't want the last day, as all vehicles will re-enter the depot
|
|
858
|
-
total_counts_by_area[vehicle_type_name][area.ID] = nv[:-SECONDS_IN_A_DAY]
|
|
859
|
-
else:
|
|
860
|
-
# This is an area for all vehicle types
|
|
861
|
-
# We don't care about this
|
|
862
|
-
continue
|
|
863
|
-
|
|
864
|
-
if False:
|
|
865
|
-
from matplotlib import pyplot as plt
|
|
866
|
-
|
|
867
|
-
for vehicle_type_name, counts in total_counts_by_area.items():
|
|
868
|
-
plt.figure()
|
|
869
|
-
for area_id, proper_counts in counts.items():
|
|
870
|
-
# dashed if direct, solid if line
|
|
871
|
-
if area_types_by_id[area_id] == AreaType.DIRECT_ONESIDE:
|
|
872
|
-
plt.plot(proper_counts, "--", label=area_id)
|
|
873
|
-
else:
|
|
874
|
-
plt.plot(proper_counts, label=area_id)
|
|
875
|
-
plt.legend()
|
|
876
|
-
plt.show()
|
|
877
|
-
|
|
878
|
-
# Calculate the maximum utilization of the direct areas and the maximum number of lines in use at the same time
|
|
879
|
-
# Per vehicle type
|
|
880
|
-
ret_val: Dict[str, Dict[AreaType, int]] = dict()
|
|
881
|
-
for vehicle_type_name, count_dicts in total_counts_by_area.items():
|
|
882
|
-
peak_direct_area_usage = 0
|
|
883
|
-
number_of_lines_in_use = 0
|
|
884
|
-
for area_id, counts in count_dicts.items():
|
|
885
|
-
if area_types_by_id[area_id] == AreaType.DIRECT_ONESIDE:
|
|
886
|
-
peak_direct_area_usage += max(peak_direct_area_usage, np.max(counts))
|
|
887
|
-
else:
|
|
888
|
-
number_of_lines_in_use += 1
|
|
889
|
-
|
|
890
|
-
ret_val[vehicle_type_name] = {
|
|
891
|
-
AreaType.DIRECT_ONESIDE: int(peak_direct_area_usage),
|
|
892
|
-
AreaType.LINE: int(number_of_lines_in_use),
|
|
893
|
-
}
|
|
894
621
|
|
|
895
|
-
|
|
622
|
+
length = vehicle_type.length
|
|
623
|
+
width = vehicle_type.width
|
|
624
|
+
|
|
625
|
+
if length is None or width is None:
|
|
626
|
+
raise ValueError(f"No length or width found for VehicleType {vehicle_type}")
|
|
627
|
+
|
|
628
|
+
# This is the angle the vehicles are parked at in direct areas
|
|
629
|
+
# zero is equivalent to the direction they would be parked in a line area
|
|
630
|
+
# 90 means they are parked perpendicular to the line and would need to turn 90 degrees to drive out
|
|
631
|
+
# This is the angle the vehicles are parked at in direct areas
|
|
632
|
+
# zero is equivalent to the direction they would be parked in a line area
|
|
633
|
+
# 90 means they are parked perpendicular to the line and would need to turn 90 degrees to drive out
|
|
634
|
+
#
|
|
635
|
+
# LINE AREA (0°):
|
|
636
|
+
# | |
|
|
637
|
+
# | |
|
|
638
|
+
#
|
|
639
|
+
# DIRECT AREA (45°):
|
|
640
|
+
# /
|
|
641
|
+
# /
|
|
642
|
+
#
|
|
643
|
+
# DIRECT AREA (90°):
|
|
644
|
+
# -
|
|
645
|
+
# -
|
|
646
|
+
angle = math.radians(angle)
|
|
647
|
+
|
|
648
|
+
match area_type:
|
|
649
|
+
case AreaType.LINE:
|
|
650
|
+
# For LINE areas, we need to round up the vehicle count to the next multiple of the standard block length
|
|
651
|
+
count = math.ceil(count / standard_block_length) * standard_block_length
|
|
652
|
+
number_of_rows = count / standard_block_length
|
|
653
|
+
|
|
654
|
+
# Return the total area, including the space between the vehicles
|
|
655
|
+
# But the space between the vehicles is only needed between, so it's one less than the count of vehicles
|
|
656
|
+
# | | | | ^
|
|
657
|
+
# | | | | | <- area_height
|
|
658
|
+
# | | | | v
|
|
659
|
+
# <-----> area_width
|
|
660
|
+
|
|
661
|
+
area_height = length * standard_block_length + (
|
|
662
|
+
spacing * (standard_block_length - 1)
|
|
663
|
+
)
|
|
664
|
+
area_width = width * number_of_rows + (
|
|
665
|
+
spacing * max((number_of_rows - 1), 0)
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
case AreaType.DIRECT_ONESIDE:
|
|
669
|
+
# Here, it's more complicated math, due to the vehicles being parked at an angle
|
|
670
|
+
#
|
|
671
|
+
# See "docs/direct_details.pdf" for a visual explanation
|
|
672
|
+
# / ^
|
|
673
|
+
# / | <- area_height
|
|
674
|
+
# / v
|
|
675
|
+
# <-> area_width
|
|
676
|
+
#
|
|
677
|
+
# - 0°
|
|
678
|
+
# / 45°
|
|
679
|
+
# | 90°
|
|
680
|
+
|
|
681
|
+
# Area height, according tho the formula in the docs
|
|
682
|
+
b_0 = (
|
|
683
|
+
math.cos(angle) * vehicle_type.width
|
|
684
|
+
+ math.sin(angle) * vehicle_type.length
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
# If the angle os too steep, refuse to calculate
|
|
688
|
+
if math.tan(angle) > vehicle_type.length / vehicle_type.width:
|
|
689
|
+
raise ValueError("The angle is too steep for the vehicle to fit")
|
|
896
690
|
|
|
691
|
+
h = (1 / math.cos(angle)) * vehicle_type.width
|
|
692
|
+
space_between = (count - 1) * math.cos(angle) * spacing
|
|
693
|
+
area_height = b_0 + (count - 1) * h + space_between
|
|
694
|
+
if count == 0:
|
|
695
|
+
area_height = 0
|
|
696
|
+
|
|
697
|
+
# Area width, according tho the formula in the docs
|
|
698
|
+
area_width = (
|
|
699
|
+
math.sin(angle) * vehicle_type.width
|
|
700
|
+
+ math.cos(angle) * vehicle_type.length
|
|
701
|
+
)
|
|
897
702
|
|
|
898
|
-
|
|
703
|
+
case AreaType.DIRECT_TWOSIDE:
|
|
704
|
+
# For DIRECT_TWOSIDE, we need to round up the vehicle count to the next even number
|
|
705
|
+
count = count + (count % 2)
|
|
706
|
+
number_of_rows = count / 2
|
|
707
|
+
|
|
708
|
+
# Here, it's more complicated math, due to the vehicles being parked at an angle
|
|
709
|
+
# See "docs/direct_details.pdf" for a visual explanation
|
|
710
|
+
# \
|
|
711
|
+
# / \
|
|
712
|
+
# / \
|
|
713
|
+
# / \
|
|
714
|
+
# / \
|
|
715
|
+
# / \
|
|
716
|
+
# /
|
|
717
|
+
raise NotImplementedError("This area type is not yet implemented")
|
|
718
|
+
|
|
719
|
+
return area_height * area_width
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def find_peak_usage(
|
|
723
|
+
depot: Depot,
|
|
724
|
+
scenario: Scenario,
|
|
725
|
+
session: sqlalchemy.orm.session.Session,
|
|
726
|
+
resolution: timedelta = timedelta(minutes=1),
|
|
727
|
+
) -> Dict[VehicleType, Dict[AreaType, int]]:
|
|
899
728
|
"""
|
|
900
|
-
|
|
729
|
+
Identifies the peak usage of the depot.
|
|
901
730
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
:
|
|
906
|
-
|
|
731
|
+
:param depot: The depot to be analyzed.
|
|
732
|
+
:param scenario: The scenario to be analyzed.
|
|
733
|
+
:param session: An open SQLAlchemy session.
|
|
734
|
+
:return: A Dict of vehicle types and the number of areas for each type.
|
|
735
|
+
"""
|
|
736
|
+
if depot.scenario_id != scenario.id:
|
|
737
|
+
raise ValueError("The scenario and depot do not match.")
|
|
738
|
+
|
|
739
|
+
# Find the first and last CHARGING_DEPOT | STANDARD_DEPARTURE events
|
|
740
|
+
event_q = (
|
|
741
|
+
session.query(Event)
|
|
742
|
+
.join(Area)
|
|
743
|
+
.join(Depot)
|
|
744
|
+
.join(VehicleType, VehicleType.id == Area.vehicle_type_id)
|
|
745
|
+
.filter(Depot.id == depot.id)
|
|
746
|
+
.filter(
|
|
747
|
+
or_(
|
|
748
|
+
Event.event_type == EventType.CHARGING_DEPOT,
|
|
749
|
+
Event.event_type == EventType.STANDBY_DEPARTURE,
|
|
750
|
+
)
|
|
751
|
+
)
|
|
752
|
+
)
|
|
753
|
+
first_event_start = event_q.order_by(Event.time_start).first().time_start
|
|
754
|
+
last_event_end = event_q.order_by(Event.time_end.desc()).first().time_end
|
|
755
|
+
|
|
756
|
+
# Create an array of occupancy at each point in time
|
|
757
|
+
peak_occupancy: Dict[VehicleType, Dict[AreaType, int]] = {}
|
|
758
|
+
timestamps = np.arange(
|
|
759
|
+
first_event_start.timestamp(),
|
|
760
|
+
last_event_end.timestamp(),
|
|
761
|
+
resolution.total_seconds(),
|
|
762
|
+
)
|
|
763
|
+
for vehicle_type in (
|
|
764
|
+
session.query(VehicleType)
|
|
765
|
+
.join(Event)
|
|
766
|
+
.join(Area)
|
|
767
|
+
.join(Depot)
|
|
768
|
+
.filter(Depot.id == depot.id)
|
|
769
|
+
.distinct()
|
|
770
|
+
):
|
|
771
|
+
peak_occupancy[vehicle_type] = {}
|
|
772
|
+
for area_type in AreaType:
|
|
773
|
+
occupancy = np.zeros(len(timestamps))
|
|
774
|
+
for event in event_q.filter(VehicleType.id == vehicle_type.id).filter(
|
|
775
|
+
Area.area_type == area_type
|
|
776
|
+
):
|
|
777
|
+
start = event.time_start.timestamp()
|
|
778
|
+
end = event.time_end.timestamp()
|
|
779
|
+
start_index = np.searchsorted(timestamps, start)
|
|
780
|
+
end_index = np.searchsorted(timestamps, end)
|
|
781
|
+
occupancy[start_index:end_index] += 1
|
|
782
|
+
peak_occupancy[vehicle_type][area_type] = int(np.max(occupancy))
|
|
783
|
+
return peak_occupancy
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def depot_smallest_possible_size(
|
|
787
|
+
station: Station,
|
|
788
|
+
scenario: Scenario,
|
|
789
|
+
session: sqlalchemy.orm.session.Session,
|
|
790
|
+
standard_block_length: int = 6,
|
|
791
|
+
charging_power: float = 90,
|
|
792
|
+
) -> Dict[VehicleType, Dict[AreaType, None | int]]:
|
|
907
793
|
"""
|
|
794
|
+
Identifies the smallest (in terms of area footprint) depot that can still fit the required vehicles.
|
|
908
795
|
|
|
909
|
-
|
|
796
|
+
This is done by first creating an "all direct" depot and identifying the amount of places needed for each vehivle
|
|
797
|
+
type. Then, rows of LINE areas are iteratively added until there are as many LINE areas (by area) as there are
|
|
798
|
+
DIRECT areas. For each count of line areas, the amount of still-needed direct areas is calculated.
|
|
910
799
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
if len(area.entry_filter.filters) > 0:
|
|
915
|
-
assert len(area.entry_filter.vehicle_types_str) == 1
|
|
916
|
-
vehicle_type_name = area.entry_filter.vehicle_types_str[0]
|
|
800
|
+
Before calling this method:
|
|
801
|
+
- An initial energy consumption simulation, creating DRIVING events for all trips, should have been run
|
|
802
|
+
- The dataset should be reduced to only contain a single depot.
|
|
917
803
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
804
|
+
Finally, the configuration with the smallest area footprint is returned.
|
|
805
|
+
|
|
806
|
+
:param station: The station where the depot is located. Rotations starting and ending at this station are considered.
|
|
807
|
+
:param scenario: The scenario to be simulated.
|
|
808
|
+
:param session: An open SQLAlchemy session.
|
|
809
|
+
:return: A dictionary of vehicle types and the number of areas for each type. This can be used as input for
|
|
810
|
+
:func:`generate_depot`.
|
|
811
|
+
"""
|
|
921
812
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
813
|
+
# Local imports to avoid circular imports
|
|
814
|
+
from eflips.depot.api import simulate_scenario
|
|
815
|
+
|
|
816
|
+
logger = logging.getLogger(__name__)
|
|
817
|
+
|
|
818
|
+
# Find all rotations starting and ending at this station
|
|
819
|
+
grouped_rotations: Dict[
|
|
820
|
+
Tuple[Station, Station], Dict[VehicleType, List[Rotation]]
|
|
821
|
+
] = group_rotations_by_start_end_stop(station.scenario_id, session)
|
|
822
|
+
|
|
823
|
+
if (station, station) not in grouped_rotations.keys():
|
|
824
|
+
raise ValueError("There are no rotations starting and ending at this station.")
|
|
825
|
+
|
|
826
|
+
vts_and_rotations: Dict[VehicleType, List[Rotation]] = grouped_rotations[
|
|
827
|
+
(station, station)
|
|
828
|
+
]
|
|
829
|
+
|
|
830
|
+
outer_savepoint = session.begin_nested()
|
|
831
|
+
|
|
832
|
+
# THis is the nested transaction for the initial "all direct" depot creation
|
|
833
|
+
savepoint = session.begin_nested()
|
|
834
|
+
|
|
835
|
+
try:
|
|
836
|
+
# Create a depot with only direct areas, one per rotation
|
|
837
|
+
vts_and_counts: Dict[VehicleType, Dict[AreaType, int]] = {}
|
|
838
|
+
for vt, rotations in vts_and_rotations.items():
|
|
839
|
+
vts_and_counts[vt] = {
|
|
840
|
+
AreaType.DIRECT_ONESIDE: len(rotations),
|
|
841
|
+
AreaType.LINE: 0,
|
|
842
|
+
AreaType.DIRECT_TWOSIDE: 0,
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
# Create the depot
|
|
846
|
+
generate_depot(
|
|
847
|
+
vts_and_counts,
|
|
848
|
+
station,
|
|
849
|
+
scenario,
|
|
850
|
+
session,
|
|
851
|
+
standard_block_length=standard_block_length,
|
|
852
|
+
cleaning_duration=None,
|
|
853
|
+
shunting_duration=None,
|
|
854
|
+
charging_power=charging_power,
|
|
855
|
+
)
|
|
856
|
+
depot = session.query(Depot).filter(Depot.scenario_id == scenario.id).one()
|
|
857
|
+
|
|
858
|
+
# Simulate the depot
|
|
859
|
+
simulate_scenario(scenario)
|
|
860
|
+
|
|
861
|
+
# Find the peak usage of the depot
|
|
862
|
+
peak_occupancies: Dict[VehicleType, Dict[AreaType, int]] = find_peak_usage(
|
|
863
|
+
depot, scenario, session
|
|
864
|
+
)
|
|
865
|
+
# Find the vehicle count for each vehicle type
|
|
866
|
+
vehicle_counts_all_direct: Dict[VehicleType, int] = dict()
|
|
867
|
+
for vehicle_type in peak_occupancies.keys():
|
|
868
|
+
vehicle_count_q = (
|
|
869
|
+
session.query(Vehicle)
|
|
870
|
+
.join(Event)
|
|
871
|
+
.join(Area)
|
|
872
|
+
.join(Depot)
|
|
873
|
+
.join(
|
|
874
|
+
VehicleType,
|
|
875
|
+
onclause=VehicleType.id == Vehicle.vehicle_type_id,
|
|
925
876
|
)
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
877
|
+
.filter(Depot.id == depot.id)
|
|
878
|
+
.filter(VehicleType.id == vehicle_type.id)
|
|
879
|
+
.distinct()
|
|
880
|
+
.count()
|
|
881
|
+
)
|
|
882
|
+
vehicle_counts_all_direct[vehicle_type] = vehicle_count_q
|
|
883
|
+
logger.debug(
|
|
884
|
+
f"Vehicle Count for {vehicle_type.name} in all-direct: {vehicle_count_q}"
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
# How many lines would that be if we go all lines?
|
|
888
|
+
max_number_of_line_areas: Dict[VehicleType, int] = dict()
|
|
889
|
+
for vt, count in peak_occupancies.items():
|
|
890
|
+
max_number_of_line_areas[vt] = math.ceil(
|
|
891
|
+
count[AreaType.DIRECT_ONESIDE] / standard_block_length
|
|
892
|
+
)
|
|
893
|
+
finally:
|
|
894
|
+
savepoint.rollback()
|
|
895
|
+
|
|
896
|
+
# Iterate over the vehicle types and line areas, calculating the total area demand.
|
|
897
|
+
|
|
898
|
+
# Store the area needed for each vehicle type and number of line areas
|
|
899
|
+
area_needed: Dict[VehicleType, Dict[int, float]] = dict()
|
|
900
|
+
occupancy_of_direct_areas: Dict[VehicleType, Dict[int, int]] = dict()
|
|
901
|
+
try:
|
|
902
|
+
for vt, rotations in vts_and_rotations.items():
|
|
903
|
+
# This is the savepoint for each vehicle type
|
|
904
|
+
savepoint = session.begin_nested()
|
|
905
|
+
|
|
906
|
+
# Remove all rotations by other vehicle types from the database. This will speed up the process.
|
|
907
|
+
# We `session.rollback()` after this, so the database is not changed.
|
|
908
|
+
for vt2 in vts_and_rotations.keys():
|
|
909
|
+
if vt2 != vt:
|
|
910
|
+
rotations = vts_and_rotations[vt2]
|
|
911
|
+
for rotation in rotations:
|
|
912
|
+
for trip in rotation.trips:
|
|
913
|
+
for event in trip.events:
|
|
914
|
+
session.delete(event)
|
|
915
|
+
for stop_time in trip.stop_times:
|
|
916
|
+
session.delete(stop_time)
|
|
917
|
+
session.delete(trip)
|
|
918
|
+
session.delete(rotation)
|
|
919
|
+
session.flush()
|
|
920
|
+
logger.debug(f"Temporarily Deleted all rotations for {vt2.name}")
|
|
921
|
+
area_needed[vt] = dict()
|
|
922
|
+
occupancy_of_direct_areas[vt] = dict()
|
|
923
|
+
for amount_of_line_areas in range(max_number_of_line_areas[vt] + 2):
|
|
924
|
+
# This is the savepoint for each number of line areas
|
|
925
|
+
inner_savepoint = session.begin_nested()
|
|
926
|
+
try:
|
|
927
|
+
# Create a depot with the given amount of line areas
|
|
928
|
+
new_vts_and_counts = {
|
|
929
|
+
vt: {
|
|
930
|
+
AreaType.LINE: amount_of_line_areas * standard_block_length,
|
|
931
|
+
AreaType.DIRECT_ONESIDE: len(vts_and_rotations[vt])
|
|
932
|
+
+ 100, # +10 to work around the "Depot is too small" error
|
|
933
|
+
AreaType.DIRECT_TWOSIDE: 0,
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
# Create the depot
|
|
938
|
+
generate_depot(
|
|
939
|
+
new_vts_and_counts,
|
|
940
|
+
station,
|
|
941
|
+
scenario,
|
|
942
|
+
session,
|
|
943
|
+
standard_block_length=standard_block_length,
|
|
944
|
+
cleaning_duration=None,
|
|
945
|
+
shunting_duration=None,
|
|
946
|
+
charging_power=charging_power,
|
|
947
|
+
)
|
|
948
|
+
depot = (
|
|
949
|
+
session.query(Depot)
|
|
950
|
+
.filter(Depot.scenario_id == scenario.id)
|
|
951
|
+
.one()
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
# Simulate the depot
|
|
955
|
+
simulate_scenario(scenario)
|
|
956
|
+
|
|
957
|
+
# Find the peak usage of the depot
|
|
958
|
+
peak_occupancies: Dict[
|
|
959
|
+
VehicleType, Dict[AreaType, int]
|
|
960
|
+
] = find_peak_usage(depot, scenario, session)
|
|
961
|
+
|
|
962
|
+
if len(peak_occupancies.keys()) != 1:
|
|
963
|
+
raise ValueError(
|
|
964
|
+
"There should only be one vehicle type in the depot"
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
peak_occupancy = peak_occupancies[vt]
|
|
968
|
+
area_for_line_areas = area_needed_for_vehicle_parking(
|
|
969
|
+
vehicle_type=vt,
|
|
970
|
+
area_type=AreaType.LINE,
|
|
971
|
+
count=peak_occupancy[AreaType.LINE],
|
|
972
|
+
standard_block_length=standard_block_length,
|
|
973
|
+
)
|
|
974
|
+
area_for_direct_areas = area_needed_for_vehicle_parking(
|
|
975
|
+
vehicle_type=vt,
|
|
976
|
+
area_type=AreaType.DIRECT_ONESIDE,
|
|
977
|
+
count=peak_occupancy[AreaType.DIRECT_ONESIDE],
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
logger.debug(
|
|
981
|
+
f"A{vt.name} in {amount_of_line_areas} line areas configuration:\n"
|
|
982
|
+
f"{area_for_line_areas:.1f} m² for line areas, {area_for_direct_areas:.1f} m² for direct areas\n"
|
|
983
|
+
f"(total: {area_for_line_areas + area_for_direct_areas:.1f} m²)\n"
|
|
984
|
+
f"Direct areas occupancy: {peak_occupancy[AreaType.DIRECT_ONESIDE]}\n"
|
|
985
|
+
f"Line areas occupancy: {peak_occupancy[AreaType.LINE]}\n"
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
# Find the vehicle count
|
|
989
|
+
vehicle_count_q = (
|
|
990
|
+
session.query(Vehicle)
|
|
991
|
+
.join(VehicleType)
|
|
992
|
+
.join(Event)
|
|
993
|
+
.join(Area)
|
|
994
|
+
.join(Depot)
|
|
995
|
+
.filter(Depot.id == depot.id)
|
|
996
|
+
.filter(VehicleType.id == vt.id)
|
|
997
|
+
.distinct()
|
|
998
|
+
.count()
|
|
999
|
+
)
|
|
1000
|
+
if vehicle_count_q <= vehicle_counts_all_direct[vt]:
|
|
1001
|
+
logger.debug(
|
|
1002
|
+
f"Vehicle count for {vt.name} in {amount_of_line_areas} line areas configuration: {vehicle_count_q}. This is <= than the all-direct configuration ({vehicle_counts_all_direct[vt]})."
|
|
1003
|
+
)
|
|
1004
|
+
occupancy_of_direct_areas[vt][
|
|
1005
|
+
amount_of_line_areas
|
|
1006
|
+
] = peak_occupancy[AreaType.DIRECT_ONESIDE]
|
|
1007
|
+
area_needed[vt][amount_of_line_areas] = (
|
|
1008
|
+
area_for_line_areas + area_for_direct_areas
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
else:
|
|
1012
|
+
logger.debug(
|
|
1013
|
+
f"Vehicle count for {vt.name} in {amount_of_line_areas} line areas configuration: {vehicle_count_q}. This is > than the all-direct configuration ({vehicle_counts_all_direct[vt]})."
|
|
1014
|
+
)
|
|
1015
|
+
except ValueError as e:
|
|
1016
|
+
if (
|
|
1017
|
+
"which suggests the fleet or the infrastructure might not be enough for the full electrification. Please add charging interfaces or increase charging power ."
|
|
1018
|
+
in repr(e)
|
|
1019
|
+
):
|
|
1020
|
+
logger.debug(f"Depot is too small.")
|
|
1021
|
+
continue
|
|
1022
|
+
finally:
|
|
1023
|
+
inner_savepoint.rollback()
|
|
1024
|
+
savepoint.rollback()
|
|
1025
|
+
# Identify the best configuration for each vehicle type
|
|
1026
|
+
ret_val: Dict[VehicleType, Dict[AreaType, int]] = dict()
|
|
1027
|
+
for vt in vts_and_rotations.keys():
|
|
1028
|
+
best_config = min(area_needed[vt].keys(), key=lambda x: area_needed[vt][x])
|
|
1029
|
+
ret_val[vt] = {
|
|
1030
|
+
AreaType.LINE: best_config * standard_block_length,
|
|
1031
|
+
AreaType.DIRECT_ONESIDE: occupancy_of_direct_areas[vt][best_config],
|
|
1032
|
+
AreaType.DIRECT_TWOSIDE: 0,
|
|
1033
|
+
}
|
|
1034
|
+
return ret_val
|
|
1035
|
+
finally:
|
|
1036
|
+
outer_savepoint.rollback()
|