From ce1a3a887d44bcbb0a32215541337696bee5c990 Mon Sep 17 00:00:00 2001 From: Shubhangi Gupta Date: Thu, 16 May 2024 15:52:29 +0200 Subject: [PATCH] algorithm modification for transshipment --- conflowgen/__init__.py | 2 +- .../container_dwell_time_analysis_report.py | 2 +- ...by_vehicle_type_analysis_summary_report.py | 2 +- ..._adjustment_per_vehicle_analysis_report.py | 2 +- ...le_capacity_utilization_analysis_report.py | 2 +- .../analyses/modal_split_analysis_report.py | 2 +- .../analyses/yard_capacity_analysis_report.py | 2 +- .../api/container_flow_generation_manager.py | 14 ++- .../container_flow_generation_properties.py | 10 +++ .../large_scheduled_vehicle_repository.py | 34 +++++++- .../container_flow_generation_service.py | 6 ++ ...hicle_for_onward_transportation_manager.py | 6 +- conflowgen/log/__init__.py | 0 conflowgen/log/log.py | 85 +++++++++++++++++++ conflowgen/reporting/__init__.py | 2 +- conflowgen/reporting/output_style.py | 2 +- .../tests/analyses/test_run_all_analyses.py | 16 ++-- .../test_container_flow_generation_manager.py | 6 +- ...d_and_outbound_vehicle_capacity_preview.py | 2 +- 19 files changed, 174 insertions(+), 23 deletions(-) create mode 100644 conflowgen/log/__init__.py create mode 100644 conflowgen/log/log.py diff --git a/conflowgen/__init__.py b/conflowgen/__init__.py index 7eaeb02f..426fa678 100644 --- a/conflowgen/__init__.py +++ b/conflowgen/__init__.py @@ -83,7 +83,7 @@ from conflowgen.domain_models.data_types.storage_requirement import StorageRequirement # List of functions -from conflowgen.logging.logging import setup_logger +from conflowgen.log.log import setup_logger from conflowgen.analyses import run_all_analyses from conflowgen.previews import run_all_previews diff --git a/conflowgen/analyses/container_dwell_time_analysis_report.py b/conflowgen/analyses/container_dwell_time_analysis_report.py index ce494cd8..63d3a5d0 100644 --- a/conflowgen/analyses/container_dwell_time_analysis_report.py +++ b/conflowgen/analyses/container_dwell_time_analysis_report.py @@ -33,7 +33,7 @@ def __init__(self): def get_report_as_text(self, **kwargs) -> str: """ - The report as a text is represented as a table suitable for logging. + The report as a text is represented as a table suitable for log. It uses a human-readable formatting style. For the exact interpretation of the parameter, check :meth:`.ContainerDwellTimeAnalysis.get_container_dwell_times`. diff --git a/conflowgen/analyses/container_flow_adjustment_by_vehicle_type_analysis_summary_report.py b/conflowgen/analyses/container_flow_adjustment_by_vehicle_type_analysis_summary_report.py index c1fb92c1..bb52d960 100644 --- a/conflowgen/analyses/container_flow_adjustment_by_vehicle_type_analysis_summary_report.py +++ b/conflowgen/analyses/container_flow_adjustment_by_vehicle_type_analysis_summary_report.py @@ -35,7 +35,7 @@ def get_report_as_text( self, **kwargs ) -> str: """ - The report as a text is represented as a table suitable for logging. + The report as a text is represented as a table suitable for log. It uses a human-readable formatting style. Keyword Args: diff --git a/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis_report.py b/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis_report.py index 3680f26d..a02d4e66 100644 --- a/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis_report.py +++ b/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis_report.py @@ -49,7 +49,7 @@ def __init__(self): def get_report_as_text(self, **kwargs) -> str: """ - The report as a text is represented as a table suitable for logging. It uses a human-readable formatting style. + The report as a text is represented as a table suitable for log. It uses a human-readable formatting style. Keyword Args: initial_vehicle_type (:obj:`typing.Any`): Either ``"all"``, a single vehicle of type diff --git a/conflowgen/analyses/inbound_to_outbound_vehicle_capacity_utilization_analysis_report.py b/conflowgen/analyses/inbound_to_outbound_vehicle_capacity_utilization_analysis_report.py index 4e82ef4e..d9742c8a 100644 --- a/conflowgen/analyses/inbound_to_outbound_vehicle_capacity_utilization_analysis_report.py +++ b/conflowgen/analyses/inbound_to_outbound_vehicle_capacity_utilization_analysis_report.py @@ -52,7 +52,7 @@ def __init__(self): def get_report_as_text(self, **kwargs) -> str: """ - The report as a text is represented as a table suitable for logging. It uses a human-readable formatting style. + The report as a text is represented as a table suitable for log. It uses a human-readable formatting style. Keyword Args: vehicle_type (:py:obj:`Any`): Either ``"scheduled vehicles"``, a single vehicle of type diff --git a/conflowgen/analyses/modal_split_analysis_report.py b/conflowgen/analyses/modal_split_analysis_report.py index 48cb36af..441e3bbd 100644 --- a/conflowgen/analyses/modal_split_analysis_report.py +++ b/conflowgen/analyses/modal_split_analysis_report.py @@ -30,7 +30,7 @@ def get_report_as_text( self, **kwargs ) -> str: """ - The report as a text is represented as a table suitable for logging. It uses a human-readable formatting style. + The report as a text is represented as a table suitable for log. It uses a human-readable formatting style. Keyword Args: start_date (datetime.datetime): diff --git a/conflowgen/analyses/yard_capacity_analysis_report.py b/conflowgen/analyses/yard_capacity_analysis_report.py index c66cc9e3..2ef81ded 100644 --- a/conflowgen/analyses/yard_capacity_analysis_report.py +++ b/conflowgen/analyses/yard_capacity_analysis_report.py @@ -40,7 +40,7 @@ def __init__(self): def get_report_as_text(self, **kwargs) -> str: """ - The report as a text is represented as a table suitable for logging. It uses a human-readable formatting style. + The report as a text is represented as a table suitable for log. It uses a human-readable formatting style. Keyword Args: storage_requirement: Either a single storage requirement of type :class:`.StorageRequirement` or a whole diff --git a/conflowgen/api/container_flow_generation_manager.py b/conflowgen/api/container_flow_generation_manager.py index 4cb3e0ea..97ca187f 100644 --- a/conflowgen/api/container_flow_generation_manager.py +++ b/conflowgen/api/container_flow_generation_manager.py @@ -27,7 +27,9 @@ def set_properties( start_date: datetime.date, end_date: datetime.date, name: typing.Optional[str] = None, - transportation_buffer: typing.Optional[float] = None + transportation_buffer: typing.Optional[float] = None, + ramp_up_period: typing.Optional[datetime.timedelta] = None, + ramp_down_period: typing.Optional[datetime.timedelta] = None, ) -> None: """ Args: @@ -38,7 +40,12 @@ def set_properties( name: The name of the generated synthetic container flow which helps to distinguish different scenarios. transportation_buffer: Determines how many percent more of the inbound journey capacity is used at most to transport containers on the outbound journey. + ramp_up_period: The period at the beginning during which operations gradually increases to full capacity. + This simulates the initial phase where container flow and terminal activities are scaled up. + ramp_down_period: The period at the end during which operations gradually decrease from full capacity. + This simulates the final phase where container flow and terminal activities are scaled down. """ + properties = self.container_flow_generation_properties_repository.get_container_flow_generation_properties() if name is not None: @@ -47,6 +54,9 @@ def set_properties( properties.start_date = start_date properties.end_date = end_date + properties.ramp_up_period = ramp_up_period.total_seconds() / 86400 # in days as float + properties.ramp_down_period = ramp_down_period.total_seconds() / 86400 # in days as float + if transportation_buffer is not None: properties.transportation_buffer = transportation_buffer @@ -67,6 +77,8 @@ def get_properties(self) -> typing.Dict[str, typing.Union[str, datetime.date, fl 'start_date': properties.start_date, 'end_date': properties.end_date, 'transportation_buffer': properties.transportation_buffer, + 'ramp_up_period': properties.ramp_up_period, + 'ramp_down_period': properties.ramp_down_period, } def container_flow_data_exists(self) -> bool: diff --git a/conflowgen/application/models/container_flow_generation_properties.py b/conflowgen/application/models/container_flow_generation_properties.py index 82a033d5..81b3a665 100644 --- a/conflowgen/application/models/container_flow_generation_properties.py +++ b/conflowgen/application/models/container_flow_generation_properties.py @@ -27,6 +27,16 @@ class ContainerFlowGenerationProperties(BaseModel): help_text="The last day of the generated container flow" ) + ramp_up_period= FloatField( + default=0, + help_text="Number of days for the ramp-up period" + ) + + ramp_down_period = FloatField( + default=0, + help_text="Number of days for the ramp-down period" + ) + generated_at = DateTimeField( default=lambda: datetime.datetime.now().replace(microsecond=0), help_text="The date the these properties have been created" diff --git a/conflowgen/domain_models/repositories/large_scheduled_vehicle_repository.py b/conflowgen/domain_models/repositories/large_scheduled_vehicle_repository.py index 05563689..dcf2989a 100644 --- a/conflowgen/domain_models/repositories/large_scheduled_vehicle_repository.py +++ b/conflowgen/domain_models/repositories/large_scheduled_vehicle_repository.py @@ -1,4 +1,7 @@ +import datetime +import enum import logging +import typing from typing import Dict, List, Callable, Type from conflowgen.domain_models.container import Container @@ -7,12 +10,19 @@ from conflowgen.domain_models.vehicle import LargeScheduledVehicle, AbstractLargeScheduledVehicle +class _Direction(enum.Enum): + inbound = 0 + outbound = 1 + + class LargeScheduledVehicleRepository: ignored_capacity = ContainerLength.get_teu_factor(ContainerLength.other) def __init__(self): self.transportation_buffer = None + self.ramp_up_period_end = None + self.ramp_down_period_start = None self.free_capacity_for_outbound_journey_buffer: Dict[Type[AbstractLargeScheduledVehicle], float] = {} self.free_capacity_for_inbound_journey_buffer: Dict[Type[AbstractLargeScheduledVehicle], float] = {} self.logger = logging.getLogger("conflowgen") @@ -21,6 +31,14 @@ def set_transportation_buffer(self, transportation_buffer: float): assert -1 < transportation_buffer self.transportation_buffer = transportation_buffer + def set_ramp_up_and_down_times( + self, + ramp_up_period_end: typing.Optional[datetime.datetime], + ramp_down_period_start: typing.Optional[datetime.datetime] + ): + self.ramp_up_period_end = ramp_up_period_end + self.ramp_down_period_start = ramp_down_period_start + def reset_cache(self): self.free_capacity_for_outbound_journey_buffer = {} self.free_capacity_for_inbound_journey_buffer = {} @@ -87,7 +105,8 @@ def get_free_capacity_for_inbound_journey(self, vehicle: Type[AbstractLargeSched free_capacity_in_teu = self._get_free_capacity_in_teu( vehicle=vehicle, maximum_capacity=total_moved_capacity_for_inbound_transportation_in_teu, - container_counter=self._get_number_containers_for_inbound_journey + container_counter=self._get_number_containers_for_inbound_journey, + direction=_Direction.inbound ) self.free_capacity_for_inbound_journey_buffer[vehicle] = free_capacity_in_teu return free_capacity_in_teu @@ -115,7 +134,8 @@ def get_free_capacity_for_outbound_journey(self, vehicle: Type[AbstractLargeSche free_capacity_in_teu = self._get_free_capacity_in_teu( vehicle=vehicle, maximum_capacity=total_moved_capacity_for_onward_transportation_in_teu, - container_counter=self._get_number_containers_for_outbound_journey + container_counter=self._get_number_containers_for_outbound_journey, + direction=_Direction.outbound ) self.free_capacity_for_outbound_journey_buffer[vehicle] = free_capacity_in_teu return free_capacity_in_teu @@ -125,7 +145,8 @@ def get_free_capacity_for_outbound_journey(self, vehicle: Type[AbstractLargeSche def _get_free_capacity_in_teu( vehicle: Type[AbstractLargeScheduledVehicle], maximum_capacity: int, - container_counter: Callable[[Type[AbstractLargeScheduledVehicle], ContainerLength], int] + container_counter: Callable[[Type[AbstractLargeScheduledVehicle], ContainerLength], int], + direction: _Direction ) -> float: loaded_20_foot_containers = container_counter(vehicle, ContainerLength.twenty_feet) loaded_40_foot_containers = container_counter(vehicle, ContainerLength.forty_feet) @@ -148,6 +169,13 @@ def _get_free_capacity_in_teu( f"loaded_40_foot_containers: {loaded_40_foot_containers}, " \ f"loaded_45_foot_containers: {loaded_45_foot_containers} and " \ f"loaded_other_containers: {loaded_other_containers}" + + arrival_time: datetime.datetime = vehicle.large_scheduled_vehicle.scheduled_arrival + if direction == _Direction.outbound and arrival_time < ramp_up_period_end: + free_capacity_in_teu = maximum_capacity * 0.1 + elif direction == _Direction.inbound and arrival_time >= ramp_down_period_start: + free_capacity_in_teu = maximum_capacity * 0.1 + return free_capacity_in_teu @classmethod diff --git a/conflowgen/flow_generator/container_flow_generation_service.py b/conflowgen/flow_generator/container_flow_generation_service.py index 9a772284..acd5b4e9 100644 --- a/conflowgen/flow_generator/container_flow_generation_service.py +++ b/conflowgen/flow_generator/container_flow_generation_service.py @@ -43,6 +43,12 @@ def _update_generation_properties_and_distributions(self): self.container_flow_end_date: datetime.date = container_flow_generation_properties.end_date assert self.container_flow_start_date < self.container_flow_end_date + ramp_up_period = container_flow_generation_properties.ramp_up_period + ramp_down_period = container_flow_generation_properties.ramp_down_period + self.ramp_up_period_end = self.container_flow_start_date + datetime.timedelta(days=ramp_up_period) + self.ramp_down_period_start = self.container_flow_end_date - datetime.timedelta(days=ramp_down_period) + assert self.ramp_up_period_end <= self.ramp_down_period_start + self.transportation_buffer: float = container_flow_generation_properties.transportation_buffer assert -1 < self.transportation_buffer diff --git a/conflowgen/flow_generator/large_scheduled_vehicle_for_onward_transportation_manager.py b/conflowgen/flow_generator/large_scheduled_vehicle_for_onward_transportation_manager.py index f0819892..a3189540 100644 --- a/conflowgen/flow_generator/large_scheduled_vehicle_for_onward_transportation_manager.py +++ b/conflowgen/flow_generator/large_scheduled_vehicle_for_onward_transportation_manager.py @@ -2,6 +2,7 @@ import datetime import logging import math +import typing from typing import Tuple, List, Dict, Type, Sequence import numpy as np @@ -42,10 +43,13 @@ def __init__(self): def reload_properties( self, - transportation_buffer: float + transportation_buffer: float, + ramp_up_period_end: typing.Optional[datetime.datetime], + ramp_down_period_start: typing.Optional[datetime.datetime], ): assert -1 < transportation_buffer self.schedule_repository.set_transportation_buffer(transportation_buffer) + self.logger.debug(f"Using transportation buffer of {transportation_buffer} when choosing the departing " f"vehicles that adhere a schedule.") diff --git a/conflowgen/log/__init__.py b/conflowgen/log/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/conflowgen/log/log.py b/conflowgen/log/log.py new file mode 100644 index 00000000..39e2e5ef --- /dev/null +++ b/conflowgen/log/log.py @@ -0,0 +1,85 @@ +import datetime +import logging +import os +import sys +from typing import Optional + +from conflowgen.tools import docstring_parameter + +LOGGING_DEFAULT_DIR = os.path.abspath( + os.path.join( + os.path.dirname(os.path.realpath(__file__)), + os.pardir, + "data", + "logs" + ) +) + +# noinspection SpellCheckingInspection +DEFAULT_LOGGING_FORMAT_STRING: str = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + + +@docstring_parameter(DEFAULT_LOGGING_FORMAT_STRING=DEFAULT_LOGGING_FORMAT_STRING) +def setup_logger( + logging_directory: Optional[str] = None, + format_string: Optional[str] = None +) -> logging.Logger: + """ + This sets up the default logger with the name 'conflowgen'. + Several classes and functions use the same logger to inform the user about the current progress. + This is just a convenience function, you can easily set up your own logger that uses the same name. + See e.g. + https://docs.python.org/3/howto/logging.html#configuring-logging + for how to set up your own logger. + + Args: + logging_directory: + The path of the directory where to store log files. + Defaults to ``/data/logs/``. + format_string: + The format string to use. + See e.g. + https://docs.python.org/3/library/logging.html#logrecord-attributes + for how to create your own format string. + Defaults to ``{DEFAULT_LOGGING_FORMAT_STRING}``. + + Returns: + The set-up logger instance. + """ + if format_string is None: + format_string = DEFAULT_LOGGING_FORMAT_STRING + + if logging_directory is None: + logging_directory = LOGGING_DEFAULT_DIR + + time_prefix = str(datetime.datetime.now()).replace(":", "-").replace(" ", "--").split(".", maxsplit=1)[0] + + formatter = logging.Formatter(format_string, datefmt="%d.%m.%Y %H:%M:%S %z") + + logger = logging.getLogger("conflowgen") + logger.setLevel(logging.DEBUG) + + stream_handlers = [handler for handler in logger.handlers if isinstance(handler, logging.StreamHandler)] + if any(handler.stream == sys.stdout for handler in stream_handlers): + logger.warning("Duplicate StreamHandler streaming to sys.stdout detected. " + "Skipping adding another StreamHandler.") + else: + stream_handler = logging.StreamHandler(stream=sys.stdout) + stream_handler.setLevel(logging.DEBUG) + stream_handler.setFormatter(formatter) + logger.addHandler(stream_handler) + + if not os.path.isdir(logging_directory): + logger.debug(f"Creating log directory at {logging_directory}") + os.makedirs(logging_directory, exist_ok=True) + path_to_log_file = os.path.join( + logging_directory, + time_prefix + ".log" + ) + logger.debug(f"Creating log file at {path_to_log_file}") + file_handler = logging.FileHandler(path_to_log_file) + file_handler.setFormatter(formatter) + file_handler.setLevel(logging.DEBUG) + logger.addHandler(file_handler) + + return logger diff --git a/conflowgen/reporting/__init__.py b/conflowgen/reporting/__init__.py index 3d2f71b4..4b5c0b81 100644 --- a/conflowgen/reporting/__init__.py +++ b/conflowgen/reporting/__init__.py @@ -67,7 +67,7 @@ def reload(self): @abc.abstractmethod def get_report_as_text(self, **kwargs) -> str: """ - The report as a text is represented as a table suitable for logging. + The report as a text is represented as a table suitable for log. It uses a human-readable formatting style. The additional keyword arguments are passed to the analysis instance in case it accepts them. diff --git a/conflowgen/reporting/output_style.py b/conflowgen/reporting/output_style.py index dae131ca..2b5da15f 100644 --- a/conflowgen/reporting/output_style.py +++ b/conflowgen/reporting/output_style.py @@ -49,7 +49,7 @@ def display_explanation(self, text: str) -> None: class DisplayAsPlainText(DisplayAsMarkupLanguage): """ With this style, the output is simply returned in a plain manner. - This is, e.g., helpful when logging the text. + This is, e.g., helpful when log the text. """ DESIRED_LINE_LENGTH = 80 # doc: The console width used for wrapping output to new lines. This is not mandatory. diff --git a/conflowgen/tests/analyses/test_run_all_analyses.py b/conflowgen/tests/analyses/test_run_all_analyses.py index fee4bdfe..4bf39f50 100644 --- a/conflowgen/tests/analyses/test_run_all_analyses.py +++ b/conflowgen/tests/analyses/test_run_all_analyses.py @@ -1,6 +1,5 @@ import datetime -import unittest -import unittest.mock +from unittest import mock from conflowgen.analyses import run_all_analyses from conflowgen.application.models.container_flow_generation_properties import ContainerFlowGenerationProperties @@ -25,8 +24,11 @@ def test_with_no_data(self): run_all_analyses() self.assertEqual(len(context.output), 35) - def test_with_no_data_as_graph(self): - with unittest.mock.patch('matplotlib.pyplot.show'): - with self.assertLogs('conflowgen', level='INFO') as context: - run_all_analyses(as_text=False, as_graph=True, static_graphs=True) - self.assertEqual(len(context.output), 27) + @mock.patch('matplotlib.pyplot.show') + #@mock.patch('conflowgen.reporting.auto_reporter.AutoReporter.present_reports') + def test_with_no_data_as_graph(self, mock_pyplot): + # with unittest.mock.patch("matplotlib.pyplot.show"): + with self.assertLogs('conflowgen', level='INFO') as context: + print("Before run_all_analyses") + run_all_analyses(as_text=False, as_graph=True, static_graphs=True) + self.assertEqual(len(context.output), 27) diff --git a/conflowgen/tests/api/test_container_flow_generation_manager.py b/conflowgen/tests/api/test_container_flow_generation_manager.py index 62c03e3c..5ccaa400 100644 --- a/conflowgen/tests/api/test_container_flow_generation_manager.py +++ b/conflowgen/tests/api/test_container_flow_generation_manager.py @@ -66,6 +66,8 @@ class MockedProperties: start_date = datetime.date(2030, 1, 1) end_date = datetime.date(2030, 12, 31) transportation_buffer = 0.2 + ramp_up_period = 10.0 + ramp_down_period = 5.0 minimum_dwell_time_of_import_containers_in_hours = 3 minimum_dwell_time_of_export_containers_in_hours = 4 minimum_dwell_time_of_transshipment_containers_in_hours = 5 @@ -77,7 +79,9 @@ class MockedProperties: 'name': "my test data", 'start_date': datetime.date(2030, 1, 1), 'end_date': datetime.date(2030, 12, 31), - 'transportation_buffer': 0.2 + 'transportation_buffer': 0.2, + 'ramp_up_period': 10.0, + 'ramp_down_period': 5.0, } with unittest.mock.patch.object( diff --git a/conflowgen/tests/previews/test_inbound_and_outbound_vehicle_capacity_preview.py b/conflowgen/tests/previews/test_inbound_and_outbound_vehicle_capacity_preview.py index 7e7e340d..f4f290e3 100644 --- a/conflowgen/tests/previews/test_inbound_and_outbound_vehicle_capacity_preview.py +++ b/conflowgen/tests/previews/test_inbound_and_outbound_vehicle_capacity_preview.py @@ -82,7 +82,7 @@ def setUp(self) -> None: def test_inbound_with_no_schedules(self): """If no schedules are provided, no capacity is needed""" - empty_capacity = self.preview.get_inbound_capacity_of_vehicles().teu + empty_capacity: dict = self.preview.get_inbound_capacity_of_vehicles().teu self.assertSetEqual(set(ModeOfTransport), set(empty_capacity.keys())) for mode_of_transport in ModeOfTransport: capacity_in_teu = empty_capacity[mode_of_transport]