eflips-depot 4.3.4__tar.gz → 4.3.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of eflips-depot might be problematic. Click here for more details.

Files changed (41) hide show
  1. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/PKG-INFO +1 -1
  2. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/api/__init__.py +728 -53
  3. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/api/private/depot.py +83 -8
  4. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/api/private/smart_charging.py +3 -20
  5. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/api/private/util.py +17 -3
  6. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/pyproject.toml +1 -1
  7. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/LICENSE.md +0 -0
  8. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/README.md +0 -0
  9. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/__init__.py +0 -0
  10. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/api/defaults/default_settings.json +0 -0
  11. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/api/private/__init__.py +0 -0
  12. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/configuration.py +0 -0
  13. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/depot.py +0 -0
  14. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/evaluation.py +0 -0
  15. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/filters.py +0 -0
  16. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/input_epex_power_price.py +0 -0
  17. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/layout_opt/__init__.py +0 -0
  18. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/layout_opt/doc/__init__.py +0 -0
  19. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/layout_opt/doc/direct_details.pdf +0 -0
  20. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/layout_opt/evaluation.py +0 -0
  21. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/layout_opt/opt_tools/__init__.py +0 -0
  22. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/layout_opt/opt_tools/crossover.py +0 -0
  23. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/layout_opt/opt_tools/fitness_c_urfd.py +0 -0
  24. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/layout_opt/opt_tools/fitness_util.py +0 -0
  25. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/layout_opt/opt_tools/init.py +0 -0
  26. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/layout_opt/opt_tools/mutation.py +0 -0
  27. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/layout_opt/optimize_c_urfd.py +0 -0
  28. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/layout_opt/packing.py +0 -0
  29. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/layout_opt/settings.py +0 -0
  30. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/layout_opt/template_creation.py +0 -0
  31. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/layout_opt/util.py +0 -0
  32. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/plots.py +0 -0
  33. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/processes.py +0 -0
  34. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/rating.py +0 -0
  35. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/resources.py +0 -0
  36. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/settings_config.py +0 -0
  37. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/simple_vehicle.py +0 -0
  38. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/simulation.py +0 -0
  39. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/smart_charging.py +0 -0
  40. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/standalone.py +0 -0
  41. {eflips_depot-4.3.4 → eflips_depot-4.3.5}/eflips/depot/validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: eflips-depot
3
- Version: 4.3.4
3
+ Version: 4.3.5
4
4
  Summary: Depot Simulation for eFLIPS
5
5
  Home-page: https://github.com/mpm-tu-berlin/eflips-depot
6
6
  License: AGPL-3.0-or-later
@@ -31,29 +31,44 @@ import logging
31
31
  import os
32
32
  import warnings
33
33
  from collections import OrderedDict
34
+ from dataclasses import dataclass
34
35
  from datetime import timedelta
35
36
  from enum import Enum
36
37
  from math import ceil
37
- from typing import Any, Dict, Optional, Union, List
38
+ from eflips.model import Process
39
+ from typing import Any, Dict, Optional, Tuple, Union, List
38
40
 
39
41
  import numpy as np
40
42
  import sqlalchemy.orm
41
43
  from eflips.model import (
42
44
  Area,
45
+ AreaType,
46
+ AssocPlanProcess,
43
47
  Depot,
44
48
  Event,
45
49
  EventType,
50
+ Plan,
46
51
  Rotation,
47
52
  Scenario,
53
+ Station,
48
54
  Trip,
49
55
  Vehicle,
56
+ VehicleType,
50
57
  )
51
58
  from sqlalchemy.orm import Session
52
59
  from sqlalchemy.sql import select
53
60
 
54
61
  import eflips.depot
55
- from eflips.depot import DepotEvaluation, ProcessStatus, SimulationHost, SimpleVehicle
62
+ from eflips.depot import (
63
+ DepotEvaluation,
64
+ DirectArea,
65
+ LineArea,
66
+ ProcessStatus,
67
+ SimulationHost,
68
+ SimpleVehicle,
69
+ )
56
70
  from eflips.depot.api.private.depot import (
71
+ _generate_all_direct_depot,
57
72
  create_simple_depot,
58
73
  delete_depots,
59
74
  depot_to_template,
@@ -68,6 +83,7 @@ from eflips.depot.api.private.util import (
68
83
  VehicleSchedule,
69
84
  check_depot_validity,
70
85
  )
86
+ from eflips.depot.evaluation import to_prev_values
71
87
 
72
88
 
73
89
  class SmartChargingStrategy(Enum):
@@ -370,12 +386,644 @@ def simple_consumption_simulation(
370
386
  session.add(current_event)
371
387
 
372
388
 
389
+ def generate_line_depot_layout(
390
+ CLEAN_DURATION: int,
391
+ charging_power: float,
392
+ station: Station,
393
+ scenario: Scenario,
394
+ session: sqlalchemy.orm.session.Session,
395
+ direct_counts: Dict[VehicleType, int],
396
+ line_counts: Dict[VehicleType, int],
397
+ line_length: int,
398
+ vehicle_type_rotation_dict: Dict[VehicleType, List[Rotation]],
399
+ shunting_duration: timedelta = timedelta(minutes=5),
400
+ ) -> None:
401
+ """
402
+ Generate a depot layout with line areas and direct areas.
403
+
404
+ :param CLEAN_DURATION: The duration of the cleaning process in seconds.
405
+ :param charging_power: The charging power of the charging area in kW.
406
+ :param station: The stop where the depot is located.
407
+ :param scenario: The scenario for which the depot layout should be generated.
408
+ :param session: The SQLAlchemy session object.
409
+ :param direct_counts: A dictionary with vehicle types as keys and the number of vehicles in the direct areas as
410
+ values.
411
+ :param line_counts: A dictionary with vehicle types as keys and the number of vehicles in the line areas as values.
412
+ :param line_length: The length of the line areas.
413
+ :param vehicle_type_rotation_dict: A dictionary with vehicle types as keys and rotations as values.
414
+ :return: The number of cleaning areas and the number of shunting areas.
415
+ """
416
+ logger = logging.getLogger(__name__)
417
+ DEBUG_PLOT = False
418
+
419
+ # In order to figure out how many cleaning areas we need, we look at the number of vehicle simultaneously being
420
+ # cleaned. This is the number of vehicles simulatenously being within the "CLEAN_DURATION" after their arrival.
421
+
422
+ # We assemble a vector of all time in the simulation
423
+ logger.info("Calculating the number of cleaning areas needed")
424
+ all_rotations = list(itertools.chain(*vehicle_type_rotation_dict.values()))
425
+ start_time = min(
426
+ [rotation.trips[0].departure_time for rotation in all_rotations]
427
+ ).timestamp()
428
+ end_time = max(
429
+ [rotation.trips[-1].arrival_time for rotation in all_rotations]
430
+ ).timestamp()
431
+ timestamps_to_sample = np.arange(start_time, end_time, 60)
432
+ clean_occupancy = np.zeros_like(timestamps_to_sample)
433
+
434
+ # Then fir each arrival, we add 1 to the CLEAN_DURATION after the arrival
435
+ for rotation in all_rotations:
436
+ rotation_end = rotation.trips[-1].arrival_time.timestamp()
437
+ clean_occupancy += np.interp(
438
+ timestamps_to_sample,
439
+ [rotation_end, rotation_end + CLEAN_DURATION],
440
+ [1, 1],
441
+ left=0,
442
+ right=0,
443
+ )
444
+
445
+ if DEBUG_PLOT:
446
+ from matplotlib import pyplot as plt
447
+
448
+ plt.figure()
449
+ plt.plot(timestamps_to_sample, clean_occupancy)
450
+ plt.show()
451
+
452
+ vehicles_arriving_in_window = int(max(clean_occupancy))
453
+ logger.info(
454
+ f"Number of vehicles arriving in a {CLEAN_DURATION/60:.1f} minute window: {vehicles_arriving_in_window:.0f}"
455
+ )
456
+
457
+ # Take a fifth of the vehicles arriving in the window as the number of cleaning areas needed
458
+ clean_areas_needed = ceil(vehicles_arriving_in_window / 2)
459
+ logger.info(f"Number of cleaning areas created: {clean_areas_needed}")
460
+ del all_rotations, clean_occupancy, timestamps_to_sample, start_time, end_time
461
+
462
+ # Create the depot
463
+ # `vehicles_arriving_in_window`+1 will be the size of our shunting areas
464
+ # `clean_areas_needed` will be the size of our cleaning areas
465
+ # We will create line and direct areas for each vehicle type
466
+ # - THe line areas will be of length `line_length` and count `line_counts[vehicle_type]`
467
+ # - The direct areas will be of length 1 and count `direct_counts[vehicle_type]`
468
+ # - The charging power for the line areas will be `charging_power_direct` and for the direct areas `charging_power`
469
+ # unless `charging_power_direct` is not set, in which case `charging_power` will be used.
470
+
471
+ # Create the depot
472
+ depot = Depot(
473
+ scenario=scenario,
474
+ name=f"Depot at {station.name}",
475
+ name_short=station.name_short,
476
+ station_id=station.id,
477
+ )
478
+ session.add(depot)
479
+
480
+ shunting_1 = Process(
481
+ name="Shunting 1 (Arrival -> Cleaning)",
482
+ scenario=scenario,
483
+ dispatchable=False,
484
+ duration=shunting_duration,
485
+ )
486
+
487
+ session.add(shunting_1)
488
+ clean = Process(
489
+ name="Arrival Cleaning",
490
+ scenario=scenario,
491
+ dispatchable=False,
492
+ duration=timedelta(seconds=CLEAN_DURATION),
493
+ )
494
+ session.add(clean)
495
+
496
+ shunting_2 = Process(
497
+ name="Shunting 2 (Cleaning -> Charging)",
498
+ scenario=scenario,
499
+ dispatchable=False,
500
+ duration=shunting_duration,
501
+ )
502
+ session.add(shunting_2)
503
+
504
+ charging = Process(
505
+ name="Charging",
506
+ scenario=scenario,
507
+ dispatchable=True,
508
+ electric_power=charging_power,
509
+ )
510
+
511
+ standby_departure = Process(
512
+ name="Standby Pre-departure",
513
+ scenario=scenario,
514
+ dispatchable=True,
515
+ )
516
+ session.add(standby_departure)
517
+
518
+ # Create shared waiting area
519
+ # This will be the "virtual" area where vehicles wait for a spot in the depot
520
+ waiting_area = Area(
521
+ scenario=scenario,
522
+ name=f"Waiting Area for every type of vehicle",
523
+ depot=depot,
524
+ area_type=AreaType.DIRECT_ONESIDE,
525
+ capacity=100,
526
+ )
527
+ session.add(waiting_area)
528
+
529
+ # Create a shared shunting area (large enough to fit all rotations)
530
+ shunting_area_1 = Area(
531
+ scenario=scenario,
532
+ name=f"Shunting Area 1 (Arrival -> Cleaning)",
533
+ depot=depot,
534
+ area_type=AreaType.DIRECT_ONESIDE,
535
+ capacity=sum(
536
+ [len(rotations) for rotations in vehicle_type_rotation_dict.values()]
537
+ ), # TODO
538
+ )
539
+ session.add(shunting_area_1)
540
+ shunting_area_1.processes.append(shunting_1)
541
+
542
+ # Create a shared cleaning area
543
+ cleaning_area = Area(
544
+ scenario=scenario,
545
+ name=f"Cleaning Area",
546
+ depot=depot,
547
+ area_type=AreaType.DIRECT_ONESIDE,
548
+ capacity=clean_areas_needed,
549
+ )
550
+ session.add(cleaning_area)
551
+ cleaning_area.processes.append(clean)
552
+
553
+ # Create a shared shunting area
554
+ shunting_area_2 = Area(
555
+ scenario=scenario,
556
+ name=f"Shunting Area 2 (Cleaning -> Charging)",
557
+ depot=depot,
558
+ area_type=AreaType.DIRECT_ONESIDE,
559
+ capacity=clean_areas_needed,
560
+ )
561
+ session.add(shunting_area_2)
562
+ shunting_area_2.processes.append(shunting_2)
563
+
564
+ # Create the line areas for each vehicle type
565
+ for vehicle_type, count in line_counts.items():
566
+ for i in range(count):
567
+ line_area = Area(
568
+ scenario=scenario,
569
+ name=f"Line Area for {vehicle_type.name} #{i+1:02d}",
570
+ depot=depot,
571
+ area_type=AreaType.LINE,
572
+ capacity=line_length,
573
+ vehicle_type=vehicle_type,
574
+ )
575
+ session.add(line_area)
576
+ line_area.processes.append(charging)
577
+ line_area.processes.append(standby_departure)
578
+
579
+ # Create the direct areas for each vehicle type
580
+ for vehicle_type, count in direct_counts.items():
581
+ if count > 0:
582
+ direct_area = Area(
583
+ scenario=scenario,
584
+ name=f"Direct Area for {vehicle_type.name}",
585
+ depot=depot,
586
+ area_type=AreaType.DIRECT_ONESIDE,
587
+ capacity=count,
588
+ vehicle_type=vehicle_type,
589
+ )
590
+ session.add(direct_area)
591
+ direct_area.processes.append(charging)
592
+ direct_area.processes.append(standby_departure)
593
+
594
+ # Create the plan
595
+ # Create plan
596
+ plan = Plan(scenario=scenario, name=f"Default Plan")
597
+ session.add(plan)
598
+
599
+ depot.default_plan = plan
600
+
601
+ # Create the assocs in order to put the areas in the plan
602
+ assocs = [
603
+ AssocPlanProcess(scenario=scenario, process=shunting_1, plan=plan, ordinal=0),
604
+ AssocPlanProcess(scenario=scenario, process=clean, plan=plan, ordinal=1),
605
+ AssocPlanProcess(scenario=scenario, process=shunting_2, plan=plan, ordinal=2),
606
+ AssocPlanProcess(scenario=scenario, process=charging, plan=plan, ordinal=3),
607
+ AssocPlanProcess(
608
+ scenario=scenario, process=standby_departure, plan=plan, ordinal=4
609
+ ),
610
+ ]
611
+ session.add_all(assocs)
612
+
613
+
614
+ @dataclass
615
+ class DepotConfiguration:
616
+ charging_power: float
617
+ line_counts: Dict[VehicleType, int]
618
+ direct_counts: Dict[VehicleType, int]
619
+ clean_duration: int
620
+ num_clean_areas: int
621
+ num_shunting_areas: int
622
+
623
+
624
+ def real_peak_area_utilization(ev: DepotEvaluation) -> Dict[str, Dict[AreaType, int]]:
625
+ """
626
+ Calculate the real peak vehicle count for a depot evaluation by vehicle type and area type.
627
+
628
+ For the line areas, the maximum number of lines in use at the same time is calculated.
629
+
630
+ :param ev: A DepotEvaluation object.
631
+ :return: The real peak vehicle count by vehicle type and area type.
632
+ """
633
+ area_types_by_id: Dict[int, AreaType] = dict()
634
+ total_counts_by_area: Dict[str, Dict[str, np.ndarray]] = dict()
635
+
636
+ # We are assuming that the smulation runs for at least four days
637
+ SECONDS_IN_A_DAY = 24 * 60 * 60
638
+ assert ev.SIM_TIME >= 4 * SECONDS_IN_A_DAY
639
+
640
+ for area in ev.depot.list_areas:
641
+ # We need to figure out which kind of area this is
642
+ # We do this by looking at the vehicle type of the area
643
+ if len(area.entry_filter.filters) > 0:
644
+ if isinstance(area, LineArea):
645
+ area_types_by_id[area.ID] = AreaType.LINE
646
+ elif isinstance(area, DirectArea):
647
+ area_types_by_id[area.ID] = AreaType.DIRECT_ONESIDE
648
+ else:
649
+ raise ValueError("Unknown area type")
650
+
651
+ assert len(area.entry_filter.vehicle_types_str) == 1
652
+ vehicle_type_name = area.entry_filter.vehicle_types_str[0]
653
+
654
+ nv = area.logger.get_valList("count", SIM_TIME=ev.SIM_TIME)
655
+ nv = to_prev_values(nv)
656
+ nv = np.array(nv)
657
+
658
+ # If the area is empty, we don't care about it
659
+ if np.all(nv == 0):
660
+ continue
661
+
662
+ if vehicle_type_name not in total_counts_by_area:
663
+ total_counts_by_area[vehicle_type_name] = dict()
664
+ # We don't want the last day, as all vehicles will re-enter the depot
665
+ total_counts_by_area[vehicle_type_name][area.ID] = nv[:-SECONDS_IN_A_DAY]
666
+ else:
667
+ # This is an area for all vehicle types
668
+ # We don't care about this
669
+ continue
670
+
671
+ if False:
672
+ from matplotlib import pyplot as plt
673
+
674
+ for vehicle_type_name, counts in total_counts_by_area.items():
675
+ plt.figure()
676
+ for area_id, proper_counts in counts.items():
677
+ # dashed if direct, solid if line
678
+ if area_types_by_id[area_id] == AreaType.DIRECT_ONESIDE:
679
+ plt.plot(proper_counts, "--", label=area_id)
680
+ else:
681
+ plt.plot(proper_counts, label=area_id)
682
+ plt.legend()
683
+ plt.show()
684
+
685
+ # Calculate the maximum utilization of the direct areas and the maximum number of lines in use at the same time
686
+ # Per vehicle type
687
+ ret_val: Dict[str, Dict[AreaType, int]] = dict()
688
+ for vehicle_type_name, count_dicts in total_counts_by_area.items():
689
+ peak_direct_area_usage = 0
690
+ number_of_lines_in_use = 0
691
+ for area_id, counts in count_dicts.items():
692
+ if area_types_by_id[area_id] == AreaType.DIRECT_ONESIDE:
693
+ peak_direct_area_usage += max(peak_direct_area_usage, np.max(counts))
694
+ else:
695
+ number_of_lines_in_use += 1
696
+
697
+ ret_val[vehicle_type_name] = {
698
+ AreaType.DIRECT_ONESIDE: int(peak_direct_area_usage),
699
+ AreaType.LINE: int(number_of_lines_in_use),
700
+ }
701
+
702
+ return ret_val
703
+
704
+
705
+ def real_peak_vehicle_count(ev: DepotEvaluation) -> Dict[str, int]:
706
+ """
707
+ Calculate the real peak vehicle count for a depot evaluation.
708
+
709
+ This is different from the amount of vehicles used
710
+ in the calculation, as towards the end of the simulation all vehicles will re-enter-the depot, which leads to
711
+ a lower actual peak vehicle count than what `nvehicles_used_calculation` returns.
712
+ :param ev: A DepotEvaluation object.
713
+ :return: The real peak vehicle count. This is what the depot layout should be designed for.
714
+ """
715
+
716
+ total_counts_by_vehicle_type: Dict[str, np.ndarray] = dict()
717
+
718
+ for area in ev.depot.list_areas:
719
+ # We need to figure out which kind of area this is
720
+ # We do this by looking at the vehicle type of the area
721
+ if len(area.entry_filter.filters) > 0:
722
+ assert len(area.entry_filter.vehicle_types_str) == 1
723
+ vehicle_type_name = area.entry_filter.vehicle_types_str[0]
724
+
725
+ nv = area.logger.get_valList("count", SIM_TIME=ev.SIM_TIME)
726
+ nv = to_prev_values(nv)
727
+ nv = np.array(nv)
728
+
729
+ if vehicle_type_name not in total_counts_by_vehicle_type:
730
+ total_counts_by_vehicle_type[vehicle_type_name] = np.zeros(
731
+ ev.SIM_TIME, dtype=np.int32
732
+ )
733
+ total_counts_by_vehicle_type[vehicle_type_name] += nv
734
+ else:
735
+ # This is an area for all vehicle types
736
+ # We don't care about this
737
+ continue
738
+
739
+ # We are assuming that the smulation runs for at least four days
740
+ SECONDS_IN_A_DAY = 24 * 60 * 60
741
+ assert ev.SIM_TIME >= 4 * SECONDS_IN_A_DAY
742
+
743
+ # Towards the end, all the vehicles will re-enter the depot
744
+ # So our practital peak vehicle count is the maximum excluding the last day
745
+ for vehicle_type_name, counts in total_counts_by_vehicle_type.items():
746
+ total_counts_by_vehicle_type[vehicle_type_name] = counts[:-SECONDS_IN_A_DAY]
747
+
748
+ return {
749
+ vehicle_type_name: int(np.max(counts))
750
+ for vehicle_type_name, counts in total_counts_by_vehicle_type.items()
751
+ }
752
+
753
+
754
+ def generate_realistic_depot_layout(
755
+ scenario: Union[Scenario, int, Any],
756
+ charging_power: float,
757
+ database_url: Optional[str] = None,
758
+ delete_existing_depot: bool = False,
759
+ line_length: int = 8,
760
+ CLEAN_DURATION: int = 10 * 60, # 10 minutes in seconds
761
+ shunting_duration: timedelta = timedelta(minutes=5),
762
+ ) -> DepotConfiguration:
763
+ """
764
+ Creates a realistic depot layout for the scenario.
765
+
766
+ This is done by starting with an all direct depot layout,
767
+ looking at the vehicle count, creating an "all line" layout and then turning some of these lines into direct
768
+ areas until the vehicle count of the all direct depot layout (+ an allowance) is reached.
769
+
770
+ :param scenario: The scenario for which the depot layout should be generated.
771
+ :param charging_power: The charging power for the line areas in kW.
772
+ :param database_url: An optional database URL. If no database URL is passed and the `scenario` parameter is not a
773
+ :class:`eflips.model.Scenario` object, the environment variable `DATABASE_URL` must be set to a valid database
774
+ URL.
775
+ :param delete_existing_depot: Whether to delete an existing depot layout for this scenario. If set to False and a
776
+ depot layout already exists, a ValueError will be raised.
777
+ :param charging_power_direct: The charging power for the direct areas in kW. If not set, the charging power for the
778
+ line areas will be used.
779
+
780
+ :return: None. The depot layout will be added to the database.
781
+ """
782
+ logging.basicConfig(level=logging.DEBUG) # TODO: Remove this line
783
+ logger = logging.getLogger(__name__)
784
+
785
+ with create_session(scenario, database_url) as (session, scenario):
786
+ # STEP 0: Delete existing depots if asked to, raise an Exception otherwise
787
+ if session.query(Depot).filter(Depot.scenario_id == scenario.id).count() != 0:
788
+ if delete_existing_depot is False:
789
+ raise ValueError("Depot already exists.")
790
+ delete_depots(scenario, session)
791
+
792
+ # Make sure that the consumption simulation has been run
793
+ if session.query(Event).filter(Event.scenario_id == scenario.id).count() == 0:
794
+ raise ValueError(
795
+ "No consumption simulation found. Please run the consumption simulation first."
796
+ )
797
+
798
+ # STEP 2: Identify the spots where we will put a depot
799
+ # Identify all the spots that serve as start *and* end of a rotation
800
+ depot_stations_and_vehicle_types = group_rotations_by_start_end_stop(
801
+ scenario.id, session
802
+ )
803
+
804
+ # STEP 3: Put "all direct" depots at these spots and find the vehicle counts
805
+ depot_stations = []
806
+ for (
807
+ first_last_stop_tup,
808
+ vehicle_type_rotation_dict,
809
+ ) in group_rotations_by_start_end_stop(scenario.id, session).items():
810
+ first_stop, last_stop = first_last_stop_tup
811
+ if first_stop != last_stop:
812
+ raise ValueError("First and last stop of a rotation are not the same.")
813
+ depot_stations.append(first_stop)
814
+
815
+ all_direct_counts: Dict[
816
+ Station, Dict[VehicleType, int]
817
+ ] = vehicle_counts_for_direct_layout(
818
+ CLEAN_DURATION=CLEAN_DURATION,
819
+ charging_power=charging_power,
820
+ stations=depot_stations,
821
+ scenario=scenario,
822
+ session=session,
823
+ vehicle_type_dict=vehicle_type_rotation_dict,
824
+ shunting_duration=shunting_duration,
825
+ )
826
+
827
+ # STEP 4: Run the simulation with depots that also have a lot of line areas
828
+ # I know I could probably skip step 3 and go directly to step 4, but that's how I got it working and
829
+ # I'm too lazy to change it now
830
+
831
+ for station, vehicle_type_and_counts in all_direct_counts.items():
832
+ line_counts: Dict[VehicleType, int] = dict()
833
+ direct_counts: Dict[VehicleType, int] = dict()
834
+
835
+ # Create a Depot that has a lot of line areas as well
836
+ for vehicle_type, count in vehicle_type_and_counts.items():
837
+ line_counts[vehicle_type] = ceil(count / line_length)
838
+ direct_counts[vehicle_type] = ceil(count) + 500
839
+
840
+ # Run the simulation with this depot
841
+ generate_line_depot_layout(
842
+ CLEAN_DURATION=CLEAN_DURATION,
843
+ charging_power=charging_power,
844
+ station=station,
845
+ scenario=scenario,
846
+ session=session,
847
+ direct_counts=direct_counts,
848
+ line_counts=line_counts,
849
+ line_length=line_length,
850
+ vehicle_type_rotation_dict=vehicle_type_rotation_dict,
851
+ shunting_duration=shunting_duration,
852
+ )
853
+
854
+ # Simulate the depot
855
+ # We will not be using add_evaluation_to_database instead taking the vehicle counts directly from the `ev` object
856
+ logger.info("Simulating the scenario")
857
+ logger.info("1/2: Initializing the simulation host")
858
+ simulation_host = init_simulation(
859
+ scenario=scenario,
860
+ session=session,
861
+ )
862
+ logger.info("2/2: Running the simulation")
863
+ depot_evaluations = run_simulation(simulation_host)
864
+
865
+ # We need to remember the depot-id-station mapping
866
+ depot_id_station_mapping: Dict[str, Station] = dict()
867
+ for depot_id_as_str, ev in depot_evaluations.items():
868
+ station = (
869
+ session.query(Station)
870
+ .join(Depot)
871
+ .filter(Depot.id == int(depot_id_as_str))
872
+ .one()
873
+ )
874
+ depot_id_station_mapping[depot_id_as_str] = station
875
+
876
+ # Delete the old depot
877
+ delete_depots(scenario, session)
878
+
879
+ for depot_id_as_str, ev in depot_evaluations.items():
880
+ assert isinstance(ev, DepotEvaluation)
881
+
882
+ if False:
883
+ ev.path_results = depot_id_as_str
884
+ os.makedirs(depot_id_as_str, exist_ok=True)
885
+
886
+ ev.vehicle_periods(
887
+ periods={
888
+ "depot general": "darkgray",
889
+ "park": "lightgray",
890
+ "Arrival Cleaning": "steelblue",
891
+ "Charging": "forestgreen",
892
+ "Standby Pre-departure": "darkblue",
893
+ "precondition": "black",
894
+ "trip": "wheat",
895
+ },
896
+ save=True,
897
+ show=False,
898
+ formats=(
899
+ "pdf",
900
+ "png",
901
+ ),
902
+ show_total_power=True,
903
+ show_annotates=True,
904
+ )
905
+
906
+ # Find the actual utilization.
907
+ utilization: Dict[str, int] = real_peak_area_utilization(ev)
908
+ utilization = {
909
+ session.query(VehicleType).filter(VehicleType.id == int(k)).one(): v
910
+ for k, v in utilization.items()
911
+ }
912
+
913
+ # Turn utilization into a two dictionaries, one for line areas and one for direct areas
914
+ for vehicle_type, counts in utilization.items():
915
+ line_counts[vehicle_type] = counts[AreaType.LINE]
916
+ direct_counts[vehicle_type] = counts[AreaType.DIRECT_ONESIDE] + 100
917
+
918
+ station = depot_id_station_mapping[depot_id_as_str]
919
+
920
+ generate_line_depot_layout(
921
+ CLEAN_DURATION=CLEAN_DURATION,
922
+ charging_power=charging_power,
923
+ station=station,
924
+ scenario=scenario,
925
+ session=session,
926
+ direct_counts=direct_counts,
927
+ line_counts=line_counts,
928
+ line_length=line_length,
929
+ vehicle_type_rotation_dict=vehicle_type_rotation_dict,
930
+ shunting_duration=shunting_duration,
931
+ )
932
+
933
+
934
+ def vehicle_counts_for_direct_layout(
935
+ CLEAN_DURATION: int,
936
+ charging_power: float,
937
+ stations: List[Station],
938
+ scenario: Scenario,
939
+ session: sqlalchemy.orm.session.Session,
940
+ vehicle_type_dict: Dict[VehicleType, List[Rotation]],
941
+ shunting_duration: timedelta = timedelta(minutes=5),
942
+ ) -> Dict[Station, Dict[VehicleType, int]]:
943
+ """
944
+ Generate a simple depot, simulate it and return the number of vehicles for each vehicle type.
945
+
946
+ Do this for each depot station in the scenario.
947
+ :param CLEAN_DURATION: The duration of the cleaning process in seconds.
948
+ :param charging_power: The charging power of the charging area in kW.
949
+ :param station: The stop where the depot is located.
950
+ :param scenario: The scenario for which the depot layout should be generated.
951
+ :param session: The SQLAlchemy session object.
952
+ :param vehicle_type_dict: A dictionary with vehicle types as keys and rotations as values.
953
+ :return: A dictionary with vehicle types as keys and the number of vehicles as values.
954
+ """
955
+ logger = logging.getLogger(__name__)
956
+
957
+ for station in stations:
958
+ logger.info(f"Generating all direct depot layout at {station.name}")
959
+ # Generate the depot
960
+ direct_counts = {}
961
+ line_counts = {}
962
+ for vehicle_type, rotations in vehicle_type_dict.items():
963
+ direct_counts[vehicle_type] = len(rotations)
964
+ line_counts[vehicle_type] = 1
965
+
966
+ generate_line_depot_layout(
967
+ CLEAN_DURATION=CLEAN_DURATION,
968
+ charging_power=charging_power,
969
+ station=station,
970
+ scenario=scenario,
971
+ session=session,
972
+ direct_counts=direct_counts,
973
+ line_counts=line_counts,
974
+ line_length=8, # We don't care about the line length here
975
+ vehicle_type_rotation_dict=vehicle_type_dict,
976
+ shunting_duration=shunting_duration,
977
+ )
978
+
979
+ # Simulate the scenario
980
+ # We will not be using add_evaluation_to_database instead taking the vehicle counts directly from the `ev` object
981
+ logger.info("Simulating the scenario")
982
+ logger.info("1/2: Initializing the simulation host")
983
+ simulation_host = init_simulation(
984
+ scenario=scenario,
985
+ session=session,
986
+ )
987
+ logger.info("2/2: Running the simulation")
988
+ depot_evaluations = run_simulation(simulation_host)
989
+
990
+ assert len(depot_evaluations) == len(stations)
991
+ depot_evaluations: Dict[str, DepotEvaluation]
992
+
993
+ ret_val: Dict[Station, Dict[VehicleType, int]] = dict()
994
+
995
+ for depot_id_as_str, ev in depot_evaluations.items():
996
+ ev = next(iter(depot_evaluations.values()))
997
+ assert isinstance(ev, DepotEvaluation)
998
+ counts: Dict[str, int] = real_peak_vehicle_count(ev)
999
+ # The key of the dictionary is the vehicle type ID as a string. We need to convert it to a vehicle type object
1000
+ vehicle_type_dict = {
1001
+ session.query(VehicleType).filter(VehicleType.id == int(k)).one(): v
1002
+ for k, v in counts.items()
1003
+ }
1004
+
1005
+ # Find the station object
1006
+ station = (
1007
+ session.query(Station)
1008
+ .join(Depot)
1009
+ .filter(Depot.id == int(depot_id_as_str))
1010
+ .one()
1011
+ )
1012
+
1013
+ ret_val[station] = vehicle_type_dict
1014
+
1015
+ # Delete the old depots
1016
+ delete_depots(scenario, session)
1017
+
1018
+ return ret_val
1019
+
1020
+
373
1021
  def generate_depot_layout(
374
1022
  scenario: Union[Scenario, int, Any],
375
1023
  charging_power: float = 150,
376
1024
  database_url: Optional[str] = None,
377
1025
  delete_existing_depot: bool = False,
378
- ):
1026
+ ) -> None:
379
1027
  """
380
1028
  Generates one or more depots for the scenario.
381
1029
 
@@ -422,51 +1070,13 @@ def generate_depot_layout(
422
1070
  first_stop, last_stop = first_last_stop_tup
423
1071
  if first_stop != last_stop:
424
1072
  raise ValueError("First and last stop of a rotation are not the same.")
425
- max_occupancies: Dict[eflips.model.VehicleType, int] = {}
426
- max_clean_occupancies: Dict[eflips.model.VehicleType, int] = {}
427
- for vehicle_type, rotations in vehicle_type_dict.items():
428
- # Slightly convoluted vehicle summation
429
- start_time = min(
430
- [rotation.trips[0].departure_time for rotation in rotations]
431
- ).timestamp()
432
- end_time = max(
433
- [rotation.trips[-1].arrival_time for rotation in rotations]
434
- ).timestamp()
435
- timestamps_to_sample = np.arange(start_time, end_time, 60)
436
- occupancy = np.zeros_like(timestamps_to_sample)
437
- clean_occupancy = np.zeros_like(timestamps_to_sample)
438
- for rotation in rotations:
439
- rotation_start = rotation.trips[0].departure_time.timestamp()
440
- rotation_end = rotation.trips[-1].arrival_time.timestamp()
441
- occupancy += np.interp(
442
- timestamps_to_sample,
443
- [rotation_start, rotation_end],
444
- [1, 1],
445
- left=0,
446
- right=0,
447
- )
448
- clean_occupancy += np.interp(
449
- timestamps_to_sample,
450
- [rotation_end, rotation_end + CLEAN_DURATION],
451
- [1, 1],
452
- left=0,
453
- right=0,
454
- )
455
- max_occupancies[vehicle_type] = max(
456
- max(occupancy), 1
457
- ) # To avoid zero occupancy
458
- max_clean_occupancies[vehicle_type] = max(max(clean_occupancy), 1)
459
-
460
- # Create a simple depot at this station
461
- create_simple_depot(
462
- scenario=scenario,
463
- station=first_stop,
464
- charging_capacities=max_occupancies,
465
- cleaning_capacities=max_clean_occupancies,
466
- charging_power=charging_power,
467
- session=session,
468
- cleaning_duration=timedelta(seconds=CLEAN_DURATION),
469
- safety_margin=0.2,
1073
+ _generate_all_direct_depot(
1074
+ CLEAN_DURATION,
1075
+ charging_power,
1076
+ first_stop,
1077
+ scenario,
1078
+ session,
1079
+ vehicle_type_dict,
470
1080
  )
471
1081
 
472
1082
 
@@ -668,6 +1278,7 @@ def init_simulation(
668
1278
  :return: A :class:`eflips.depot.Simulation.SimulationHost` object. This object should be reagrded as a "black box"
669
1279
  by the user. It should be passed to :func:`run_simulation()` to run the simulation and obtain the results.
670
1280
  """
1281
+
671
1282
  # Clear the eflips settings
672
1283
  eflips.settings.reset_settings()
673
1284
 
@@ -675,7 +1286,7 @@ def init_simulation(
675
1286
 
676
1287
  # Step 1: Set up the depot
677
1288
  eflips_depots = []
678
- for depot in scenario.depots:
1289
+ for depot in session.query(Depot).filter(Depot.scenario_id == scenario.id).all():
679
1290
  # Step 1.5: Check validity of a depot
680
1291
  check_depot_validity(depot)
681
1292
 
@@ -708,7 +1319,9 @@ def init_simulation(
708
1319
  scenario=scenario,
709
1320
  session=session,
710
1321
  )
711
- for rotation in scenario.rotations
1322
+ for rotation in session.query(Rotation)
1323
+ .filter(Rotation.scenario_id == scenario.id)
1324
+ .all()
712
1325
  ]
713
1326
 
714
1327
  first_departure_time = min(
@@ -746,8 +1359,11 @@ def init_simulation(
746
1359
  simulation_host.timetable = timetable
747
1360
 
748
1361
  # Step 4: Set up the vehicle types
1362
+ # Clear old vehicle counts, if they exist
1363
+ eflips.globalConstants["depot"]["vehicle_count"] = {}
1364
+
749
1365
  # We need to calculate roughly how many vehicles we need for each depot
750
- for depot in scenario.depots:
1366
+ for depot in session.query(Depot).filter(Depot.scenario_id == scenario.id).all():
751
1367
  depot_id = str(depot.id)
752
1368
  eflips.globalConstants["depot"]["vehicle_count"][depot_id] = {}
753
1369
  vehicle_types_for_depot = set(str(area.vehicle_type_id) for area in depot.areas)
@@ -784,7 +1400,9 @@ def init_simulation(
784
1400
  ] = (vehicle_count * 2)
785
1401
 
786
1402
  # We need to put the vehicle type objects into the GlobalConstants
787
- for vehicle_type in scenario.vehicle_types:
1403
+ for vehicle_type in (
1404
+ session.query(VehicleType).filter(VehicleType.scenario_id == scenario.id).all()
1405
+ ):
788
1406
  eflips.globalConstants["depot"]["vehicle_types"][
789
1407
  str(vehicle_type.id)
790
1408
  ] = vehicle_type_to_global_constants_dict(vehicle_type)
@@ -870,6 +1488,57 @@ def _add_evaluation_to_database(
870
1488
  )
871
1489
 
872
1490
 
1491
+ def insert_dummy_standby_departure_events(depot_id: int, session: Session) -> None:
1492
+ """
1493
+ Workaround for the missing STANDBY_DEPARTURE events in the database.
1494
+
1495
+ :param session: The database session
1496
+ :param scenario: A scenario object
1497
+ :return:
1498
+ """
1499
+ logger = logging.getLogger(__name__)
1500
+
1501
+ # Look for charging events at areas belonging to the depot
1502
+ charging_events = (
1503
+ session.query(Event)
1504
+ .join(Area)
1505
+ .filter(Area.depot_id == depot_id)
1506
+ .filter(Event.event_type == EventType.CHARGING_DEPOT)
1507
+ .all()
1508
+ )
1509
+
1510
+ for charging_event in charging_events:
1511
+ # See if the next event is a DRIVING event, but there is time between the two events
1512
+ next_event = (
1513
+ session.query(Event)
1514
+ .filter(Event.time_start >= charging_event.time_end)
1515
+ .filter(Event.vehicle_id == charging_event.vehicle_id)
1516
+ .order_by(Event.time_start)
1517
+ .first()
1518
+ )
1519
+ if (
1520
+ next_event is not None
1521
+ and next_event.event_type == EventType.DRIVING
1522
+ and (next_event.time_start - charging_event.time_end) > timedelta(seconds=1)
1523
+ ):
1524
+ logger.warning("Inserting dummy STANDBY_DEPARTURE event")
1525
+ # Insert a dummy STANDBY_DEPARTURE event
1526
+ dummy_event = Event(
1527
+ vehicle_id=charging_event.vehicle_id,
1528
+ vehicle_type_id=charging_event.vehicle.vehicle_type_id,
1529
+ time_start=charging_event.time_end,
1530
+ time_end=(next_event.time_start - timedelta(seconds=1)),
1531
+ event_type=EventType.STANDBY_DEPARTURE,
1532
+ area_id=charging_event.area_id,
1533
+ subloc_no=charging_event.subloc_no,
1534
+ scenario_id=charging_event.scenario_id,
1535
+ soc_start=charging_event.soc_end,
1536
+ soc_end=charging_event.soc_end,
1537
+ description="Dummy STANDBY_DEPARTURE event",
1538
+ )
1539
+ session.add(dummy_event)
1540
+
1541
+
873
1542
  def add_evaluation_to_database(
874
1543
  scenario: Scenario,
875
1544
  depot_evaluations: Dict[str, DepotEvaluation],
@@ -903,7 +1572,7 @@ def add_evaluation_to_database(
903
1572
 
904
1573
  waiting_area_id = None
905
1574
 
906
- total_areas = scenario.areas
1575
+ total_areas = session.query(Area).filter(Area.scenario_id == scenario.id).all()
907
1576
  for area in total_areas:
908
1577
  if area.depot_id == int(depot_id) and len(area.processes) == 0:
909
1578
  waiting_area_id = area.id
@@ -1352,6 +2021,8 @@ def _add_events_into_database(
1352
2021
 
1353
2022
  :return: None. The results are added to the database.
1354
2023
  """
2024
+ logger = logging.getLogger(__name__)
2025
+
1355
2026
  for start_time, process_dict in dict_of_events.items():
1356
2027
  # Generate EventType
1357
2028
  match process_dict["type"]:
@@ -1377,6 +2048,10 @@ def _add_events_into_database(
1377
2048
  '"Standby", "Precondition"'
1378
2049
  )
1379
2050
 
2051
+ if process_dict["end"] == start_time:
2052
+ logger.warning("Refusing to create an event with zero duration.")
2053
+ continue
2054
+
1380
2055
  current_event = Event(
1381
2056
  scenario=scenario,
1382
2057
  vehicle_type_id=db_vehicle.vehicle_type_id,
@@ -5,6 +5,9 @@ from enum import Enum, auto
5
5
  from math import ceil
6
6
  from typing import Dict, List, Tuple
7
7
 
8
+ import eflips.model
9
+ import numpy as np
10
+
8
11
  import sqlalchemy.orm
9
12
  from eflips.model import (
10
13
  Scenario,
@@ -99,12 +102,15 @@ def depot_to_template(depot: Depot) -> Dict[str, str | Dict[str, str | int]]:
99
102
  }
100
103
 
101
104
  # Fill in vehicle_filter.
102
- template["areas"][area_name]["entry_filter"] = {
103
- "filter_names": ["vehicle_type"],
104
- "vehicle_types": [str(area.vehicle_type_id)],
105
- }
106
-
107
- # TODO for cleaning area etc., enable non-vehicle_type areas
105
+ # If the vehicle type id is set, the area is only for this vehicle type
106
+ if area.vehicle_type_id is not None:
107
+ template["areas"][area_name]["entry_filter"] = {
108
+ "filter_names": ["vehicle_type"],
109
+ "vehicle_types": [str(area.vehicle_type_id)],
110
+ }
111
+ else:
112
+ # If the vehicle type id is not set, the area is for all vehicle types
113
+ template["areas"][area_name]["entry_filter"] = dict()
108
114
 
109
115
  for process in area.processes:
110
116
  # Add process into process list
@@ -298,6 +304,7 @@ def create_simple_depot(
298
304
  session: sqlalchemy.orm.session.Session,
299
305
  cleaning_duration: timedelta = timedelta(minutes=30),
300
306
  safety_margin: float = 0.0,
307
+ shunting_duration: timedelta = timedelta(minutes=5),
301
308
  ) -> None:
302
309
  """
303
310
  Creates a simple depot for a given scenario.
@@ -336,7 +343,7 @@ def create_simple_depot(
336
343
  name="Shunting 1",
337
344
  scenario=scenario,
338
345
  dispatchable=False,
339
- duration=timedelta(minutes=5),
346
+ duration=shunting_duration,
340
347
  )
341
348
  clean = Process(
342
349
  name="Arrival Cleaning",
@@ -348,7 +355,7 @@ def create_simple_depot(
348
355
  name="Shunting 2",
349
356
  scenario=scenario,
350
357
  dispatchable=False,
351
- duration=timedelta(minutes=5),
358
+ duration=shunting_duration,
352
359
  )
353
360
  charging = Process(
354
361
  name="Charging",
@@ -505,3 +512,71 @@ def process_type(p: Process) -> ProcessType:
505
512
  return ProcessType.STANDBY
506
513
  else:
507
514
  raise ValueError("Invalid process type")
515
+
516
+
517
+ def _generate_all_direct_depot(
518
+ CLEAN_DURATION: int,
519
+ charging_power: float,
520
+ first_stop: Station,
521
+ scenario: Scenario,
522
+ session: sqlalchemy.orm.session.Session,
523
+ vehicle_type_dict: Dict[VehicleType, List[Rotation]],
524
+ shunting_duration: timedelta = timedelta(minutes=5),
525
+ ) -> None:
526
+ """
527
+ Private inner function to generate a depot layout with an arrival and a charging area for each vehicle type.
528
+
529
+ :param CLEAN_DURATION: The duration of the cleaning process in seconds.
530
+ :param charging_power: The charging power of the charging area in kW.
531
+ :param first_stop: The stop where the depot is located.
532
+ :param scenario: The scenario for which the depot layout should be generated.
533
+ :param session: The SQLAlchemy session object.
534
+ :param vehicle_type_dict: A dictionary with vehicle types as keys and rotations as values.
535
+ :return: Nothing. The depot layout is created in the database.
536
+ """
537
+ max_occupancies: Dict[eflips.model.VehicleType, int] = {}
538
+ max_clean_occupancies: Dict[eflips.model.VehicleType, int] = {}
539
+ for vehicle_type, rotations in vehicle_type_dict.items():
540
+ # Slightly convoluted vehicle summation
541
+ start_time = min(
542
+ [rotation.trips[0].departure_time for rotation in rotations]
543
+ ).timestamp()
544
+ end_time = max(
545
+ [rotation.trips[-1].arrival_time for rotation in rotations]
546
+ ).timestamp()
547
+ timestamps_to_sample = np.arange(start_time, end_time, 60)
548
+ occupancy = np.zeros_like(timestamps_to_sample)
549
+ clean_occupancy = np.zeros_like(timestamps_to_sample)
550
+ for rotation in rotations:
551
+ rotation_start = rotation.trips[0].departure_time.timestamp()
552
+ rotation_end = rotation.trips[-1].arrival_time.timestamp()
553
+ occupancy += np.interp(
554
+ timestamps_to_sample,
555
+ [rotation_start, rotation_end],
556
+ [1, 1],
557
+ left=0,
558
+ right=0,
559
+ )
560
+ clean_occupancy += np.interp(
561
+ timestamps_to_sample,
562
+ [rotation_end, rotation_end + CLEAN_DURATION],
563
+ [1, 1],
564
+ left=0,
565
+ right=0,
566
+ )
567
+ max_occupancies[vehicle_type] = max(
568
+ max(occupancy), 1
569
+ ) # To avoid zero occupancy
570
+ max_clean_occupancies[vehicle_type] = max(max(clean_occupancy), 1)
571
+ # Create a simple depot at this station
572
+ create_simple_depot(
573
+ scenario=scenario,
574
+ station=first_stop,
575
+ charging_capacities=max_occupancies,
576
+ cleaning_capacities=max_clean_occupancies,
577
+ charging_power=charging_power,
578
+ session=session,
579
+ cleaning_duration=timedelta(seconds=CLEAN_DURATION),
580
+ safety_margin=0.2,
581
+ shunting_duration=shunting_duration,
582
+ )
@@ -135,21 +135,9 @@ def optimize_charging_events_even(charging_events: List[Event]) -> None:
135
135
  optimized_power, params_for_event["max_power"]
136
136
  )
137
137
 
138
- # Count the energy that is we need to distribute over the time when the vehicle is not at peak power
139
- energy_to_distribute = (
140
- np.trapz((optimized_power - optimized_power_capped), total_time) / 3600
141
- ) # kWh
142
- if energy_to_distribute > 0:
143
- optimized_power_not_capped = np.where(
144
- optimized_power > optimized_power_capped
145
- )
146
- # Distribute the energy over the time when the vehicle is not at peak power
147
- uncapped_duration = (
148
- optimized_power_not_capped[0].shape[0]
149
- * TEMPORAL_RESOLUTION.total_seconds()
150
- )
151
- power_to_add = energy_to_distribute / (uncapped_duration / 3600)
152
- optimized_power[optimized_power_not_capped[0]] += power_to_add
138
+ if not np.all(optimized_power_capped != optimized_power):
139
+ # If the power draw is capped, we will just use the mean power draw
140
+ optimized_power = params_for_event["charging_allowed"] * mean_power
153
141
 
154
142
  # Make sure the transferred energy is the same
155
143
  post_opt_energy = (
@@ -221,15 +209,10 @@ def optimize_charging_events_even(charging_events: List[Event]) -> None:
221
209
  params_for_event["transferred_energy"] / post_opt_energy
222
210
  )
223
211
 
224
- optimized_power2 = (
225
- params_for_event["charging_allowed"] * mean_power
226
- ) # * (mean_occupancy / total_occupancy)
227
212
  # Fill NaNs with the zero power draw
228
213
  optimized_power[np.isnan(optimized_power)] = 0
229
- optimized_power2[np.isnan(optimized_power2)] = 0
230
214
 
231
215
  params_for_event["optimized_power"] = optimized_power
232
- params_for_event["optimized_power2"] = optimized_power2
233
216
 
234
217
  event = params_for_event["event"]
235
218
  start_index = int((event.time_start - start_time) / TEMPORAL_RESOLUTION)
@@ -1,5 +1,5 @@
1
1
  """This module contains miscellaneous utility functions for the eflips-depot API."""
2
-
2
+ import logging
3
3
  import os
4
4
  from contextlib import contextmanager
5
5
  from dataclasses import dataclass
@@ -40,6 +40,8 @@ def create_session(
40
40
  database, or any other object that has an attribute `id` that is an integer.
41
41
  :return: Yield a Tuple of the session and the scenario.
42
42
  """
43
+ logger = logging.getLogger(__name__)
44
+
43
45
  managed_session = False
44
46
  engine = None
45
47
  session = None
@@ -47,6 +49,10 @@ def create_session(
47
49
  if isinstance(scenario, Scenario):
48
50
  session = inspect(scenario).session
49
51
  elif isinstance(scenario, int) or hasattr(scenario, "id"):
52
+ logger.warning(
53
+ "Scenario passed was not part of an active session. Uncommited changes will be ignored."
54
+ )
55
+
50
56
  if isinstance(scenario, int):
51
57
  scenario_id = scenario
52
58
  else:
@@ -348,8 +354,16 @@ class VehicleSchedule:
348
354
  opportunity_charging = rot.allow_opportunity_charging
349
355
 
350
356
  # Find the depot at the start and end of the rotation
351
- start_depot = trips[0].route.departure_station.depot
352
- end_depot = trips[-1].route.arrival_station.depot
357
+ start_depot = (
358
+ session.query(Depot)
359
+ .filter(Depot.station_id == trips[0].route.departure_station_id)
360
+ .one()
361
+ )
362
+ end_depot = (
363
+ session.query(Depot)
364
+ .filter(Depot.station_id == trips[-1].route.arrival_station_id)
365
+ .one()
366
+ )
353
367
 
354
368
  if start_depot is None or end_depot is None:
355
369
  raise ValueError(f"Rotation {rot.id} has no depot at the start or end.")
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "eflips-depot"
3
- version = "4.3.4"
3
+ version = "4.3.5"
4
4
  description = "Depot Simulation for eFLIPS"
5
5
  authors = ["Enrico Lauth <enrico.lauth@tu-berlin.de>",
6
6
  "Ludger Heide <ludger.heide@tu-berlin.de",
File without changes
File without changes