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.
@@ -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 _generate_all_direct_depot(
525
- CLEAN_DURATION: int,
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
- direct_counts: Dict[VehicleType, int],
599
- line_counts: Dict[VehicleType, int],
600
- line_length: int,
601
- vehicle_type_rotation_dict: Dict[VehicleType, List[Rotation]],
602
- shunting_duration: timedelta = timedelta(minutes=5),
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
- Generate a depot layout with line areas and direct areas.
606
-
607
- :param CLEAN_DURATION: The duration of the cleaning process in seconds.
608
- :param charging_power: The charging power of the charging area in kW.
609
- :param station: The stop where the depot is located.
610
- :param scenario: The scenario for which the depot layout should be generated.
611
- :param session: The SQLAlchemy session object.
612
- :param direct_counts: A dictionary with vehicle types as keys and the number of vehicles in the direct areas as
613
- values.
614
- :param line_counts: A dictionary with vehicle types as keys and the number of vehicles in the line areas as values.
615
- :param line_length: The length of the line areas.
616
- :param vehicle_type_rotation_dict: A dictionary with vehicle types as keys and rotations as values.
617
- :return: The number of cleaning areas and the number of shunting areas.
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
- if DEBUG_PLOT:
649
- from matplotlib import pyplot as plt
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
- plt.figure()
652
- plt.plot(timestamps_to_sample, clean_occupancy)
653
- plt.show()
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
- vehicles_arriving_in_window = int(max(clean_occupancy))
656
- logger.info(
657
- f"Number of vehicles arriving in a {CLEAN_DURATION/60:.1f} minute window: {vehicles_arriving_in_window:.0f}"
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
- # Take a fifth of the vehicles arriving in the window as the number of cleaning areas needed
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
- shunting_1 = Process(
684
- name="Shunting 1 (Arrival -> Cleaning)",
685
- scenario=scenario,
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
- session.add(shunting_1)
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
- shunting_2 = Process(
700
- name="Shunting 2 (Cleaning -> Charging)",
701
- scenario=scenario,
702
- dispatchable=False,
703
- duration=shunting_duration,
704
- )
705
- session.add(shunting_2)
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
- # Create a shared shunting area (large enough to fit all rotations)
733
- shunting_area_1 = Area(
734
- scenario=scenario,
735
- name=f"Shunting Area 1 (Arrival -> Cleaning)",
736
- depot=depot,
737
- area_type=AreaType.DIRECT_ONESIDE,
738
- capacity=sum(
739
- [len(rotations) for rotations in vehicle_type_rotation_dict.values()]
740
- ), # TODO
741
- )
742
- session.add(shunting_area_1)
743
- shunting_area_1.processes.append(shunting_1)
744
-
745
- # Create a shared cleaning area
746
- cleaning_area = Area(
747
- scenario=scenario,
748
- name=f"Cleaning Area",
749
- depot=depot,
750
- area_type=AreaType.DIRECT_ONESIDE,
751
- capacity=clean_areas_needed,
752
- )
753
- session.add(cleaning_area)
754
- cleaning_area.processes.append(clean)
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"Line Area for {vehicle_type.name} #{i+1:02d}",
564
+ name=f"Direct Area for {vehicle_type.name_short}",
773
565
  depot=depot,
774
- area_type=AreaType.LINE,
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
- session.add(line_area)
779
- line_area.processes.append(charging)
780
- line_area.processes.append(standby_departure)
781
-
782
- # Create the direct areas for each vehicle type
783
- for vehicle_type, count in direct_counts.items():
784
- if count > 0:
785
- direct_area = Area(
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.name}",
580
+ name=f"Direct Area for {vehicle_type.name_short}",
788
581
  depot=depot,
789
- area_type=AreaType.DIRECT_ONESIDE,
790
- capacity=count,
582
+ area_type=AreaType.DIRECT_TWOSIDE,
791
583
  vehicle_type=vehicle_type,
584
+ capacity=capacities[AreaType.DIRECT_TWOSIDE],
792
585
  )
793
- session.add(direct_area)
794
- direct_area.processes.append(charging)
795
- direct_area.processes.append(standby_departure)
586
+ area.processes.append(charging)
587
+ area.processes.append(standby_departure)
588
+ session.add(area)
796
589
 
797
- # Create the plan
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
- # Create the assocs in order to put the areas in the plan
805
- assocs = [
806
- AssocPlanProcess(scenario=scenario, process=shunting_1, plan=plan, ordinal=0),
807
- AssocPlanProcess(scenario=scenario, process=clean, plan=plan, ordinal=1),
808
- AssocPlanProcess(scenario=scenario, process=shunting_2, plan=plan, ordinal=2),
809
- AssocPlanProcess(scenario=scenario, process=charging, plan=plan, ordinal=3),
810
- AssocPlanProcess(
811
- scenario=scenario, process=standby_departure, plan=plan, ordinal=4
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
- def real_peak_area_utilization(ev: DepotEvaluation) -> Dict[str, Dict[AreaType, int]]:
818
- """
819
- Calculate the real peak vehicle count for a depot evaluation by vehicle type and area type.
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 line areas, the maximum number of lines in use at the same time is calculated.
611
+ For the DIRECT area types, an angle of 45° is assumed for the vehicles.
822
612
 
823
- :param ev: A DepotEvaluation object.
824
- :return: The real peak vehicle count by vehicle type and area type.
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
- return ret_val
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
- def real_peak_vehicle_count(ev: DepotEvaluation) -> Dict[str, int]:
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
- Calculate the real peak vehicle count for a depot evaluation.
729
+ Identifies the peak usage of the depot.
901
730
 
902
- This is different from the amount of vehicles used
903
- in the calculation, as towards the end of the simulation all vehicles will re-enter-the depot, which leads to
904
- a lower actual peak vehicle count than what `nvehicles_used_calculation` returns.
905
- :param ev: A DepotEvaluation object.
906
- :return: The real peak vehicle count. This is what the depot layout should be designed for.
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
- total_counts_by_vehicle_type: Dict[str, np.ndarray] = dict()
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
- for area in ev.depot.list_areas:
912
- # We need to figure out which kind of area this is
913
- # We do this by looking at the vehicle type of the area
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
- nv = area.logger.get_valList("count", SIM_TIME=ev.SIM_TIME)
919
- nv = to_prev_values(nv)
920
- nv = np.array(nv)
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
- if vehicle_type_name not in total_counts_by_vehicle_type:
923
- total_counts_by_vehicle_type[vehicle_type_name] = np.zeros(
924
- ev.SIM_TIME, dtype=np.int32
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
- total_counts_by_vehicle_type[vehicle_type_name] += nv
927
- else:
928
- # This is an area for all vehicle types
929
- # We don't care about this
930
- continue
931
-
932
- # We are assuming that the smulation runs for at least four days
933
- SECONDS_IN_A_DAY = 24 * 60 * 60
934
- assert ev.SIM_TIME >= 4 * SECONDS_IN_A_DAY
935
-
936
- # Towards the end, all the vehicles will re-enter the depot
937
- # So our practital peak vehicle count is the maximum excluding the last day
938
- for vehicle_type_name, counts in total_counts_by_vehicle_type.items():
939
- total_counts_by_vehicle_type[vehicle_type_name] = counts[:-SECONDS_IN_A_DAY]
940
-
941
- return {
942
- vehicle_type_name: int(np.max(counts))
943
- for vehicle_type_name, counts in total_counts_by_vehicle_type.items()
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()