diff --git a/.gitignore b/.gitignore index f1eb5052..487aaeb9 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,4 @@ examples/Python_Script/databases/ # Ignore local changes as they happen with every execution. If something changes, the commit must be forced. conflowgen/data/tools/ docs/notebooks/data/prepared_dbs/ +.tools/ diff --git a/.tools/.gitkeep b/.tools/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/conflowgen/__init__.py b/conflowgen/__init__.py index 7eaeb02f..1ab3baa9 100644 --- a/conflowgen/__init__.py +++ b/conflowgen/__init__.py @@ -35,10 +35,10 @@ InboundAndOutboundVehicleCapacityAnalysis from conflowgen.analyses.inbound_and_outbound_vehicle_capacity_analysis_report import \ InboundAndOutboundVehicleCapacityAnalysisReport -from conflowgen.analyses.inbound_to_outbound_vehicle_capacity_utilization_analysis import \ - InboundToOutboundVehicleCapacityUtilizationAnalysis -from conflowgen.analyses.inbound_to_outbound_vehicle_capacity_utilization_analysis_report import \ - InboundToOutboundVehicleCapacityUtilizationAnalysisReport +from conflowgen.analyses.outbound_to_inbound_vehicle_capacity_utilization_analysis import \ + OutboundToInboundVehicleCapacityUtilizationAnalysis +from conflowgen.analyses.outbound_to_inbound_vehicle_capacity_utilization_analysis_report import \ + OutboundToInboundVehicleCapacityUtilizationAnalysisReport from conflowgen.analyses.container_flow_by_vehicle_type_analysis import ContainerFlowByVehicleTypeAnalysis from conflowgen.analyses.container_flow_by_vehicle_type_analysis_report import \ ContainerFlowByVehicleTypeAnalysisReport @@ -64,6 +64,9 @@ ContainerFlowVehicleTypeAdjustmentPerVehicleAnalysis from conflowgen.analyses.container_flow_vehicle_type_adjustment_per_vehicle_analysis_report import \ ContainerFlowVehicleTypeAdjustmentPerVehicleAnalysisReport +from conflowgen.analyses.container_flow_by_vehicle_instance_analysis import ContainerFlowByVehicleInstanceAnalysis +from conflowgen.analyses.container_flow_by_vehicle_instance_analysis_report import ( + ContainerFlowByVehicleInstanceAnalysisReport) # Cache for analyses and previews from conflowgen.data_summaries.data_summaries_cache import DataSummariesCache @@ -83,7 +86,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/__init__.py b/conflowgen/analyses/__init__.py index 000026d6..5cb8f4a2 100644 --- a/conflowgen/analyses/__init__.py +++ b/conflowgen/analyses/__init__.py @@ -7,12 +7,13 @@ ContainerFlowAdjustmentByVehicleTypeAnalysisReport from .container_flow_adjustment_by_vehicle_type_analysis_summary_report import \ ContainerFlowAdjustmentByVehicleTypeAnalysisSummaryReport +from .container_flow_by_vehicle_instance_analysis_report import ContainerFlowByVehicleInstanceAnalysisReport from .container_flow_by_vehicle_type_analysis_report import ContainerFlowByVehicleTypeAnalysisReport from .container_flow_vehicle_type_adjustment_per_vehicle_analysis_report import \ ContainerFlowVehicleTypeAdjustmentPerVehicleAnalysisReport from .inbound_and_outbound_vehicle_capacity_analysis_report import InboundAndOutboundVehicleCapacityAnalysisReport -from .inbound_to_outbound_vehicle_capacity_utilization_analysis_report import \ - InboundToOutboundVehicleCapacityUtilizationAnalysisReport +from .outbound_to_inbound_vehicle_capacity_utilization_analysis_report import \ + OutboundToInboundVehicleCapacityUtilizationAnalysisReport from .modal_split_analysis_report import ModalSplitAnalysisReport from .quay_side_throughput_analysis_report import QuaySideThroughputAnalysisReport from .truck_gate_throughput_analysis_report import TruckGateThroughputAnalysisReport @@ -26,6 +27,7 @@ reports: typing.Iterable[typing.Type[AbstractReport]] = [ InboundAndOutboundVehicleCapacityAnalysisReport, ContainerFlowByVehicleTypeAnalysisReport, + ContainerFlowByVehicleInstanceAnalysisReport, ContainerFlowAdjustmentByVehicleTypeAnalysisReport, ContainerFlowAdjustmentByVehicleTypeAnalysisSummaryReport, ContainerFlowVehicleTypeAdjustmentPerVehicleAnalysisReport, @@ -34,7 +36,7 @@ QuaySideThroughputAnalysisReport, TruckGateThroughputAnalysisReport, YardCapacityAnalysisReport, - InboundToOutboundVehicleCapacityUtilizationAnalysisReport + OutboundToInboundVehicleCapacityUtilizationAnalysisReport ] diff --git a/conflowgen/analyses/abstract_analysis.py b/conflowgen/analyses/abstract_analysis.py index e8e466eb..c5fbad4d 100644 --- a/conflowgen/analyses/abstract_analysis.py +++ b/conflowgen/analyses/abstract_analysis.py @@ -98,35 +98,39 @@ def _restrict_storage_requirement(selected_containers: ModelSelect, storage_requ @staticmethod def _restrict_container_delivered_by_vehicle_type( selected_containers: ModelSelect, container_delivered_by_vehicle_type: typing.Any - ) -> ModelSelect: + ) -> (ModelSelect, list[ModeOfTransport]): if hashable(container_delivered_by_vehicle_type) \ and container_delivered_by_vehicle_type in set(ModeOfTransport): selected_containers = selected_containers.where( Container.delivered_by == container_delivered_by_vehicle_type ) + list_of_vehicle_types = [container_delivered_by_vehicle_type] else: # assume it is some kind of collection (list, set, ...) selected_containers = selected_containers.where( Container.delivered_by << container_delivered_by_vehicle_type ) - return selected_containers + list_of_vehicle_types = list(container_delivered_by_vehicle_type) + return selected_containers, list_of_vehicle_types @staticmethod def _restrict_container_picked_up_by_vehicle_type( selected_containers: ModelSelect, container_picked_up_by_vehicle_type: typing.Any - ) -> ModelSelect: + ) -> (ModelSelect, list[ModeOfTransport]): if container_picked_up_by_vehicle_type == "scheduled vehicles": container_picked_up_by_vehicle_type = ModeOfTransport.get_scheduled_vehicles() - + list_of_vehicle_types = container_picked_up_by_vehicle_type if hashable(container_picked_up_by_vehicle_type) \ and container_picked_up_by_vehicle_type in set(ModeOfTransport): selected_containers = selected_containers.where( Container.picked_up_by == container_picked_up_by_vehicle_type ) + list_of_vehicle_types = [container_picked_up_by_vehicle_type] else: # assume it is some kind of collection (list, set, ...) selected_containers = selected_containers.where( Container.picked_up_by << container_picked_up_by_vehicle_type ) - return selected_containers + list_of_vehicle_types = list(container_picked_up_by_vehicle_type) + return selected_containers, list_of_vehicle_types @staticmethod def _restrict_container_picked_up_by_initial_vehicle_type( diff --git a/conflowgen/analyses/container_dwell_time_analysis.py b/conflowgen/analyses/container_dwell_time_analysis.py index 822e42b7..8a30db70 100644 --- a/conflowgen/analyses/container_dwell_time_analysis.py +++ b/conflowgen/analyses/container_dwell_time_analysis.py @@ -64,12 +64,12 @@ def get_container_dwell_times( ) if container_delivered_by_vehicle_type != "all": - selected_containers = self._restrict_container_delivered_by_vehicle_type( + selected_containers, vehicle_types = self._restrict_container_delivered_by_vehicle_type( selected_containers, container_delivered_by_vehicle_type ) if container_picked_up_by_vehicle_type != "all": - selected_containers = self._restrict_container_picked_up_by_vehicle_type( + selected_containers, vehicle_types = self._restrict_container_picked_up_by_vehicle_type( selected_containers, container_picked_up_by_vehicle_type ) 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_by_vehicle_instance_analysis.py b/conflowgen/analyses/container_flow_by_vehicle_instance_analysis.py new file mode 100644 index 00000000..4e92e4d6 --- /dev/null +++ b/conflowgen/analyses/container_flow_by_vehicle_instance_analysis.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +import datetime +import typing + +from peewee import ModelSelect + +from conflowgen.data_summaries.data_summaries_cache import DataSummariesCache +from conflowgen.domain_models.container import Container +from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport +from conflowgen.analyses.abstract_analysis import AbstractAnalysis +from conflowgen.descriptive_datatypes import VehicleIdentifier, FlowDirection + + +class ContainerFlowByVehicleInstanceAnalysis(AbstractAnalysis): + """ + This analysis can be run after the synthetic data has been generated. + The analysis returns a data structure that can be used for generating reports (e.g., in text or as a figure) + as it is the case with :class:`.ContainerFlowByVehicleInstanceAnalysisReport`. + """ + + @DataSummariesCache.cache_result + def get_container_flow_by_vehicle( + self, + vehicle_types: typing.Collection[ModeOfTransport] = ( + ModeOfTransport.train, + ModeOfTransport.feeder, + ModeOfTransport.deep_sea_vessel, + ModeOfTransport.barge + ), + start_date: typing.Optional[datetime.datetime] = None, + end_date: typing.Optional[datetime.datetime] = None, + ) -> [ + ModeOfTransport, typing.Dict[VehicleIdentifier, typing.Dict[FlowDirection, (int, int)]] + ]: + """ + Args: + vehicle_types: A collection of vehicle types, e.g., passed as a :class:`list` or :class:`set`. + Only the vehicles that correspond to the provided vehicle type(s) are considered in the analysis. + start_date: + The earliest arriving container that is included. Consider all containers if :obj:`None`. + end_date: + The latest departing container that is included. Consider all containers if :obj:`None`. + + Returns: + Grouped by vehicle type and vehicle instance, how many import, export, and transshipment containers are + unloaded and loaded (measured in TEU). + """ + + container_flow_by_vehicle: typing.Dict[ + ModeOfTransport, typing.Dict[VehicleIdentifier, + typing.Dict[FlowDirection, typing.Dict[str, int]]]] = { + vehicle_type: {} + for vehicle_type in ModeOfTransport + } + + vehicle_identifier_cache = {} + + selected_containers: ModelSelect = Container.select().where( + (Container.delivered_by.in_(vehicle_types) | Container.picked_up_by.in_(vehicle_types)) + ) + + container: Container + for container in selected_containers: + if start_date and container.get_arrival_time() < start_date: + continue + if end_date and container.get_departure_time() > end_date: + continue + + if container.delivered_by_large_scheduled_vehicle is not None: # if not transported by truck + + if container.delivered_by_large_scheduled_vehicle not in vehicle_identifier_cache: + vehicle_id_inbound = VehicleIdentifier( + id=container.delivered_by_large_scheduled_vehicle.id, + mode_of_transport=container.delivered_by, + service_name=container.delivered_by_large_scheduled_vehicle.schedule.service_name, + vehicle_name=container.delivered_by_large_scheduled_vehicle.vehicle_name, + vehicle_arrival_time=container.get_arrival_time(), + ) + vehicle_identifier_cache[container.delivered_by_large_scheduled_vehicle] = vehicle_id_inbound + + vehicle_id_inbound = vehicle_identifier_cache[container.delivered_by_large_scheduled_vehicle] + + if vehicle_id_inbound not in container_flow_by_vehicle[container.delivered_by]: + container_flow_by_vehicle[container.delivered_by][vehicle_id_inbound] = { + flow_direction: { + "inbound": 0, + "outbound": 0, + } + for flow_direction in FlowDirection + } + container_flow_by_vehicle[ + container.delivered_by + ][vehicle_id_inbound][container.flow_direction]["inbound"] += container.occupied_teu + + if container.picked_up_by_large_scheduled_vehicle is not None: # if not transported by truck + + if container.picked_up_by_large_scheduled_vehicle not in vehicle_identifier_cache: + vehicle_id_outbound = VehicleIdentifier( + id=container.picked_up_by_large_scheduled_vehicle.id, + mode_of_transport=container.picked_up_by, + service_name=container.picked_up_by_large_scheduled_vehicle.schedule.service_name, + vehicle_name=container.picked_up_by_large_scheduled_vehicle.vehicle_name, + vehicle_arrival_time=container.get_departure_time(), + ) + vehicle_identifier_cache[container.picked_up_by_large_scheduled_vehicle] = vehicle_id_outbound + + vehicle_id_outbound = vehicle_identifier_cache[container.picked_up_by_large_scheduled_vehicle] + + if vehicle_id_outbound not in container_flow_by_vehicle[container.picked_up_by]: + container_flow_by_vehicle[container.picked_up_by][vehicle_id_outbound] = { + flow_direction: { + "inbound": 0, + "outbound": 0, + } + for flow_direction in FlowDirection + } + container_flow_by_vehicle[ + container.picked_up_by][vehicle_id_outbound][container.flow_direction]["outbound"] += 1 + + for skipped_vehicle_type in set(ModeOfTransport) - set(vehicle_types): + assert len(container_flow_by_vehicle[skipped_vehicle_type]) == 0 + del container_flow_by_vehicle[skipped_vehicle_type] + + return container_flow_by_vehicle diff --git a/conflowgen/analyses/container_flow_by_vehicle_instance_analysis_report.py b/conflowgen/analyses/container_flow_by_vehicle_instance_analysis_report.py new file mode 100644 index 00000000..a6327e61 --- /dev/null +++ b/conflowgen/analyses/container_flow_by_vehicle_instance_analysis_report.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +import logging +import typing + +import matplotlib.figure +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd + +from conflowgen.analyses.container_flow_by_vehicle_instance_analysis import ContainerFlowByVehicleInstanceAnalysis +from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport +from conflowgen.reporting import AbstractReportWithMatplotlib +from conflowgen.reporting.no_data_plot import no_data_graph + + +class ContainerFlowByVehicleInstanceAnalysisReport(AbstractReportWithMatplotlib): + """ + This analysis report takes the data structure as generated by :class:`.ContainerFlowByVehicleInstanceAnalysis` + and creates a comprehensible representation for the user, either as text or as a graph. + The visual and table are expected to approximately look like in the + `example ContainerFlowByVehicleInstanceAnalysisReport \ + `_. + """ + + report_description = """ + Analyze how many import, export, and transshipment containers were unloaded and loaded on each vehicle. + """ + + logger = logging.getLogger("conflowgen") + + def __init__(self): + super().__init__() + self._df = None + self.analysis = ContainerFlowByVehicleInstanceAnalysis() + + def get_report_as_text( + self, + vehicle_types: ModeOfTransport | str | typing.Collection = "scheduled vehicles", + **kwargs + ) -> str: + """ + Keyword Args: + vehicle_types (typing.Collection[ModeOfTransport]): A collection of vehicle types, e.g., passed as a + :class:`list` or :class:`set`. + Only the vehicles that correspond to the provided vehicle type(s) are considered in the analysis. + start_date (datetime.datetime): The earliest arriving container that is included. + Consider all containers if :obj:`None`. + end_date (datetime.datetime): The latest departing container that is included. + Consider all containers if :obj:`None`. + + Returns: + The report in human-readable text format + """ + plain_table, vehicle_types = self._get_analysis(kwargs) + + if len(plain_table) > 0: + df = self._get_dataframe_from_plain_table(plain_table) + report = str(df) + else: + report = "(no report feasible because no data is available)" + + return report + + def _get_analysis(self, kwargs: dict) -> (list, list): + start_date = kwargs.pop("start_date", None) + end_date = kwargs.pop("end_date", None) + vehicle_types = kwargs.pop("vehicle_types", ( + ModeOfTransport.train, + ModeOfTransport.feeder, + ModeOfTransport.deep_sea_vessel, + ModeOfTransport.barge + )) + + assert len(kwargs) == 0, f"Keyword(s) {kwargs.keys()} have not been processed" + + container_flow = self.analysis.get_container_flow_by_vehicle( + vehicle_types=vehicle_types, + start_date=start_date, + end_date=end_date, + ) + + vehicle_types = list(container_flow.keys()) + + plain_table = [] + + for mode_of_transport in vehicle_types: + for vehicle_identifier in container_flow[mode_of_transport].keys(): + for flow_direction in container_flow[mode_of_transport][vehicle_identifier]: + for journey_direction in container_flow[mode_of_transport][vehicle_identifier][flow_direction]: + handled_volume = container_flow[ + mode_of_transport][vehicle_identifier][flow_direction][journey_direction] + + plain_table.append( + (mode_of_transport, vehicle_identifier.id, vehicle_identifier.vehicle_name, + vehicle_identifier.service_name, vehicle_identifier.vehicle_arrival_time, + str(flow_direction), journey_direction, handled_volume) + ) + + return plain_table, vehicle_types + + def get_report_as_graph(self, **kwargs) -> matplotlib.figure.Figure: + """ + Keyword Args: + vehicle_types (typing.Collection[ModeOfTransport]): A collection of vehicle types, e.g., passed as a + :class:`list` or :class:`set`. + Only the vehicles that correspond to the provided vehicle type(s) are considered in the analysis. + start_date (datetime.datetime): + The earliest arriving container that is included. Consider all containers if :obj:`None`. + end_date (datetime.datetime): + The latest departing container that is included. Consider all containers if :obj:`None`. + + Returns: + Grouped by vehicle type and vehicle instance, how many import, export, and transshipment containers are + unloaded and loaded (measured in TEU). + """ + plain_table, vehicle_types = self._get_analysis(kwargs) + + plot_title = "Container Flow By Vehicle Instance Analysis Report" + + if len(plain_table) == 0: + fig, ax = no_data_graph() + ax.set_title(plot_title) + return fig + + df = self._get_dataframe_from_plain_table(plain_table) + + number_subplots = 2 * len(vehicle_types) + fig, axes = plt.subplots(nrows=number_subplots, figsize=(7, 4 * number_subplots)) + i = 0 + for mode_of_transport in vehicle_types: + for journey_direction in ["inbound", "outbound"]: + ax = axes[i] + i += 1 + ax.set_title(f"{str(mode_of_transport).replace('_', ' ').capitalize()} - {journey_direction}") + df[(df["mode_of_transport"] == mode_of_transport) + & (df["journey_direction"] == journey_direction) + ].groupby("flow_direction")["handled_volume"].plot(ax=ax, linestyle=":", marker=".") + ax.set_ylabel("") + + plt.legend(loc='center left', bbox_to_anchor=(1, 0.5)) + + plt.tight_layout() + + return fig + + def _get_dataframe_from_plain_table(self, plain_table): + column_names = ("mode_of_transport", "vehicle_id", "vehicle_name", "service_name", "vehicle_arrival_time", + "flow_direction", "journey_direction", "handled_volume") + df = pd.DataFrame(plain_table) + df.columns = column_names + df.set_index("vehicle_arrival_time", inplace=True) + df.replace(0, np.nan, inplace=True) + self._df = df + return df diff --git a/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis.py b/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis.py index 0d5a0589..f77ea071 100644 --- a/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis.py +++ b/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis.py @@ -61,7 +61,7 @@ def get_vehicle_type_adjustments_per_vehicle( ) if adjusted_vehicle_type is not None and adjusted_vehicle_type != "all": - selected_containers = self._restrict_container_picked_up_by_vehicle_type( + selected_containers, list_of_vehicle_types = self._restrict_container_picked_up_by_vehicle_type( selected_containers, adjusted_vehicle_type ) @@ -99,6 +99,7 @@ def get_vehicle_type_adjustments_per_vehicle( def _get_vehicle_identifier_for_vehicle_picking_up_the_container(container: Container) -> VehicleIdentifier: if container.picked_up_by == ModeOfTransport.truck: vehicle_identifier = VehicleIdentifier( + id=None, mode_of_transport=ModeOfTransport.truck, vehicle_arrival_time=container.get_departure_time(), service_name=None, @@ -106,6 +107,7 @@ def _get_vehicle_identifier_for_vehicle_picking_up_the_container(container: Cont ) else: vehicle_identifier = VehicleIdentifier( + id=container.picked_up_by_large_scheduled_vehicle.id, mode_of_transport=container.picked_up_by, vehicle_arrival_time=container.get_departure_time(), service_name=container.picked_up_by_large_scheduled_vehicle.schedule.service_name, 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..a8d226a2 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 @@ -10,7 +10,7 @@ from conflowgen.analyses.container_flow_vehicle_type_adjustment_per_vehicle_analysis import \ ContainerFlowVehicleTypeAdjustmentPerVehicleAnalysis -from conflowgen.analyses.inbound_to_outbound_vehicle_capacity_utilization_analysis import VehicleIdentifier +from conflowgen.analyses.outbound_to_inbound_vehicle_capacity_utilization_analysis import VehicleIdentifier from conflowgen.reporting import AbstractReportWithMatplotlib from conflowgen.reporting.no_data_plot import no_data_graph @@ -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_and_outbound_vehicle_capacity_analysis.py b/conflowgen/analyses/inbound_and_outbound_vehicle_capacity_analysis.py index 638eab61..46a03e14 100644 --- a/conflowgen/analyses/inbound_and_outbound_vehicle_capacity_analysis.py +++ b/conflowgen/analyses/inbound_and_outbound_vehicle_capacity_analysis.py @@ -109,7 +109,7 @@ def get_outbound_container_volume_by_vehicle_type( large_scheduled_vehicle: LargeScheduledVehicle for large_scheduled_vehicle in LargeScheduledVehicle.select(): maximum_capacity_of_vehicle = min( - int(large_scheduled_vehicle.moved_capacity * (1 + self.transportation_buffer)), + int(large_scheduled_vehicle.inbound_container_volume * (1 + self.transportation_buffer)), large_scheduled_vehicle.capacity_in_teu ) vehicle_type: ModeOfTransport = large_scheduled_vehicle.schedule.vehicle_type diff --git a/conflowgen/analyses/modal_split_analysis_report.py b/conflowgen/analyses/modal_split_analysis_report.py index 48cb36af..1f149d9e 100644 --- a/conflowgen/analyses/modal_split_analysis_report.py +++ b/conflowgen/analyses/modal_split_analysis_report.py @@ -18,8 +18,8 @@ class ModalSplitAnalysisReport(AbstractReportWithMatplotlib): """ report_description = """ - Analyze the amount of containers dedicated for or coming from the hinterland compared to the amount of containers - that are transshipment. + Analyze how many containers are delivered and picked up by which type of vehicle. + This counts the containers in the yard, i.e., transshipment containers are *not* counted twice. """ def __init__(self): @@ -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/inbound_to_outbound_vehicle_capacity_utilization_analysis.py b/conflowgen/analyses/outbound_to_inbound_vehicle_capacity_utilization_analysis.py similarity index 95% rename from conflowgen/analyses/inbound_to_outbound_vehicle_capacity_utilization_analysis.py rename to conflowgen/analyses/outbound_to_inbound_vehicle_capacity_utilization_analysis.py index 33626555..f84df5db 100644 --- a/conflowgen/analyses/inbound_to_outbound_vehicle_capacity_utilization_analysis.py +++ b/conflowgen/analyses/outbound_to_inbound_vehicle_capacity_utilization_analysis.py @@ -23,7 +23,7 @@ class InboundAndOutboundCapacity(typing.NamedTuple): outbound_capacity: float -class InboundToOutboundVehicleCapacityUtilizationAnalysis(AbstractAnalysis): +class OutboundToInboundVehicleCapacityUtilizationAnalysis(AbstractAnalysis): """ This analysis can be run after the synthetic data has been generated. The analysis returns a data structure that can be used for generating reports (e.g., in text or as a figure) @@ -66,7 +66,7 @@ def get_inbound_and_outbound_capacity_of_each_vehicle( # vehicle properties vehicle_name = vehicle.vehicle_name vehicle_arrival_time = vehicle.get_arrival_time() - used_capacity_on_inbound_journey = vehicle.moved_capacity + used_capacity_on_inbound_journey = vehicle.inbound_container_volume if start_date and vehicle_arrival_time < start_date: continue @@ -87,6 +87,7 @@ def get_inbound_and_outbound_capacity_of_each_vehicle( used_capacity_on_outbound_journey += container.occupied_teu vehicle_id = VehicleIdentifier( + id=vehicle.id, mode_of_transport=mode_of_transport, service_name=service_name, vehicle_name=vehicle_name, diff --git a/conflowgen/analyses/inbound_to_outbound_vehicle_capacity_utilization_analysis_report.py b/conflowgen/analyses/outbound_to_inbound_vehicle_capacity_utilization_analysis_report.py similarity index 96% rename from conflowgen/analyses/inbound_to_outbound_vehicle_capacity_utilization_analysis_report.py rename to conflowgen/analyses/outbound_to_inbound_vehicle_capacity_utilization_analysis_report.py index 4e82ef4e..c88e44dd 100644 --- a/conflowgen/analyses/inbound_to_outbound_vehicle_capacity_utilization_analysis_report.py +++ b/conflowgen/analyses/outbound_to_inbound_vehicle_capacity_utilization_analysis_report.py @@ -9,8 +9,8 @@ import pandas as pd from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport -from conflowgen.analyses.inbound_to_outbound_vehicle_capacity_utilization_analysis import \ - InboundToOutboundVehicleCapacityUtilizationAnalysis, VehicleIdentifier +from conflowgen.analyses.outbound_to_inbound_vehicle_capacity_utilization_analysis import \ + OutboundToInboundVehicleCapacityUtilizationAnalysis, VehicleIdentifier from conflowgen.reporting import AbstractReportWithMatplotlib from conflowgen.reporting.no_data_plot import no_data_graph @@ -19,13 +19,13 @@ class UnsupportedPlotTypeException(Exception): pass -class InboundToOutboundVehicleCapacityUtilizationAnalysisReport(AbstractReportWithMatplotlib): +class OutboundToInboundVehicleCapacityUtilizationAnalysisReport(AbstractReportWithMatplotlib): """ This analysis report takes the data structure as generated by :class:`.InboundToOutboundCapacityUtilizationAnalysis` and creates a comprehensible representation for the user, either as text or as a graph. The visual and table are expected to approximately look like in the `example InboundToOutboundVehicleCapacityUtilizationAnalysisReport \ - `_. + `_. """ report_description = """ @@ -45,14 +45,14 @@ def __init__(self): self._start_date = None self._vehicle_type_description = None self.vehicle_type_description = None - self.analysis = InboundToOutboundVehicleCapacityUtilizationAnalysis( + self.analysis = OutboundToInboundVehicleCapacityUtilizationAnalysis( transportation_buffer=self.transportation_buffer ) self._df = None 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/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..ada21a3a 100644 --- a/conflowgen/api/container_flow_generation_manager.py +++ b/conflowgen/api/container_flow_generation_manager.py @@ -8,6 +8,7 @@ ContainerFlowGenerationPropertiesRepository from conflowgen.flow_generator.container_flow_generation_service import \ ContainerFlowGenerationService +from conflowgen.metadata import __version__ class ContainerFlowGenerationManager: @@ -27,7 +28,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 +41,13 @@ 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 yard occupancy gradually increases. + During the ramp-up period, the share of transshipment containers on outbound journey is reduced. + ramp_down_period: The period at the end during which operations gradually fade out. + During the ramp-down period, inbound container volumes are scaled down, reducing the number of new + import, export, and transshipment containers in the yard during this period.. """ + properties = self.container_flow_generation_properties_repository.get_container_flow_generation_properties() if name is not None: @@ -47,9 +56,21 @@ def set_properties( properties.start_date = start_date properties.end_date = end_date + if ramp_up_period: + properties.ramp_up_period = ramp_up_period.total_seconds() / 86400 # in days as float + else: + properties.ramp_up_period = 0 + + if ramp_down_period: + properties.ramp_down_period = ramp_down_period.total_seconds() / 86400 # in days as float + else: + properties.ramp_down_period = 0 + if transportation_buffer is not None: properties.transportation_buffer = transportation_buffer + properties.conflowgen_version = __version__ + self.container_flow_generation_properties_repository.set_container_flow_generation_properties( properties ) @@ -67,6 +88,9 @@ 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, + 'conflowgen_version': properties.conflowgen_version } def container_flow_data_exists(self) -> bool: diff --git a/conflowgen/api/port_call_manager.py b/conflowgen/api/port_call_manager.py index 5da8191d..46b8d9ee 100644 --- a/conflowgen/api/port_call_manager.py +++ b/conflowgen/api/port_call_manager.py @@ -1,6 +1,7 @@ from __future__ import annotations import datetime import typing +import uuid from conflowgen.data_summaries.data_summaries_cache import DataSummariesCache from conflowgen.domain_models.factories.schedule_factory import ScheduleFactory @@ -22,20 +23,21 @@ class PortCallManager: def __init__(self): self.schedule_factory = ScheduleFactory() - def add_vehicle( + def add_service_that_calls_terminal( self, vehicle_type: ModeOfTransport, service_name: str, vehicle_arrives_at: datetime.date, vehicle_arrives_at_time: datetime.time, average_vehicle_capacity: int, - average_moved_capacity: int, + average_inbound_container_volume: int, next_destinations: typing.Optional[typing.List[typing.Tuple[str, float]]] = None, - vehicle_arrives_every_k_days: typing.Optional[int] = None + vehicle_arrives_every_k_days: int = 7 ) -> None: r""" - Add the schedule of a ship of any size or a train. The concrete vehicle instances are automatically generated - when :meth:`.ContainerFlowGenerationManager.generate` is invoked. + Add a service that frequently calls the terminal, both on the seaside and landside. + The vehicle instances are automatically generated when :meth:`.ContainerFlowGenerationManager.generate` is + invoked. Args: vehicle_type: One of @@ -55,10 +57,10 @@ def add_vehicle( Number of TEU that can be transported with the vehicle at most. The number of moved containers can never exceed this number, no matter what the value for the ``transportation_buffer`` is set to. - average_moved_capacity: - The average moved capacity describes the number of TEU which the vehicle delivers to the terminal on its - inbound journey. - When summing up the TEU factors of each of the loaded containers on the inbound journey, this value is + average_inbound_container_volume: + The average moved container volume describes the number of TEU which the vehicle delivers to the + terminal on its inbound journey. + When summing up the TEU values of each of the loaded containers on the inbound journey, this value is never exceeded but closely approximated. For the outbound journey, the containers are assigned depending on the distribution kept in :class:`.ModeOfTransportDistributionManager`. @@ -67,13 +69,14 @@ def add_vehicle( .. math:: min( - \text{average_moved_capacity} \cdot \text{transportation_buffer},\text{average_vehicle_capacity} + \text{average_inbound_container_volume} \cdot \text{transportation_buffer}, + \text{average_vehicle_capacity} ) If you have calibrated the aforementioned distribution accordingly, the actual number of containers on the outbound journey in TEU should be on average the same as on the inbound journey. - In that case, the vehicle moves ``average_moved_capacity`` number of containers in TEU on its inbound - journey and the same number of containers in TEU again on its outbound journey. + In that case, the vehicle moves ``average_inbound_container_volume`` number of containers in TEU on its + inbound journey and the same number of containers in TEU again on its outbound journey. next_destinations: Pairs of destination and frequency of the destination being chosen. vehicle_arrives_every_k_days: @@ -98,12 +101,81 @@ def add_vehicle( vehicle_arrives_at=vehicle_arrives_at, vehicle_arrives_at_time=vehicle_arrives_at_time, average_vehicle_capacity=average_vehicle_capacity, - average_moved_capacity=average_moved_capacity, + average_inbound_container_volume=average_inbound_container_volume, next_destinations=next_destinations, vehicle_arrives_every_k_days=vehicle_arrives_every_k_days ) DataSummariesCache.reset_cache() + def add_vehicle( + self, + vehicle_type: ModeOfTransport, + vehicle_arrives_at: datetime.date, + vehicle_arrives_at_time: datetime.time, + vehicle_capacity: int, + inbound_container_volume: int, + next_destinations: typing.Optional[typing.List[typing.Tuple[str, float]]] = None, + service_name: typing.Optional[str] = None, + ) -> None: + r""" + Add a service that frequently calls the terminal, both on the seaside and landside. + The vehicle instances are automatically generated when :meth:`.ContainerFlowGenerationManager.generate` is + invoked. + + Args: + vehicle_type: One of + :class:`ModeOfTransport.deep_sea_vessel`, + :class:`ModeOfTransport.feeder`, + :class:`ModeOfTransport.barge`, or + :class:`ModeOfTransport.train` + service_name: + The name of the service, i.e., the shipping line or rail freight line. + Defaults to a randomly generated id. + vehicle_arrives_at: + A date the service would arrive at the terminal. This can, e.g., point at the week day for weekly + services. In any case, this is combined with the parameter ``vehicle_arrives_every_k_days`` and only + arrivals within the time scope between ``start_date`` and ``end_date`` are considered. + vehicle_arrives_at_time: + A time of the day (between 00:00 and 23:59). + vehicle_capacity: + Number of TEU that can be transported with the vehicle at most. + The number of moved containers can never exceed this number, no matter what the value for the + ``transportation_buffer`` is set to. + inbound_container_volume: + This describes the number of TEU which the vehicle delivers to the + terminal on its inbound journey. + When summing up the TEU values of each of the loaded containers on the inbound journey, this value is + never exceeded but closely approximated. + For the outbound journey, the containers are assigned depending on the distribution kept in + :class:`.ModeOfTransportDistributionManager`. + The maximum number of containers in TEU on the outbound journey of the vehicle is bound by + + .. math:: + + min( + \text{average_inbound_container_volume} \cdot \text{transportation_buffer}, + \text{average_vehicle_capacity} + ) + + If you have calibrated the aforementioned distribution accordingly, the actual number of containers on + the outbound journey in TEU should be on average the same as on the inbound journey. + In that case, the vehicle moves ``average_inbound_container_volume`` number of containers in TEU on its + inbound journey and the same number of containers in TEU again on its outbound journey. + next_destinations: + Pairs of destination and frequency of the destination being chosen. + """ + + return self.add_service_that_calls_terminal( + vehicle_type=vehicle_type, + service_name=service_name if service_name is not None else str(uuid.uuid4()), + vehicle_arrives_at=vehicle_arrives_at, + vehicle_arrives_at_time=vehicle_arrives_at_time, + average_vehicle_capacity=vehicle_capacity, + average_inbound_container_volume=inbound_container_volume, + next_destinations=next_destinations, + vehicle_arrives_every_k_days=-1 + ) + def has_schedule( self, service_name: str, diff --git a/conflowgen/application/models/container_flow_generation_properties.py b/conflowgen/application/models/container_flow_generation_properties.py index 82a033d5..4651d002 100644 --- a/conflowgen/application/models/container_flow_generation_properties.py +++ b/conflowgen/application/models/container_flow_generation_properties.py @@ -4,6 +4,7 @@ from conflowgen.domain_models.seeders import DEFAULT_TRANSPORTATION_BUFFER from conflowgen.domain_models.base_model import BaseModel +from conflowgen.metadata import __version__ class ContainerFlowGenerationProperties(BaseModel): @@ -14,7 +15,7 @@ class ContainerFlowGenerationProperties(BaseModel): name = CharField( null=True, - help_text="The name of the generated container flow, e.g. a scenario" + help_text="The name of the generated container flow, e.g., describing the scenario" ) start_date = DateField( @@ -27,6 +28,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" @@ -39,3 +50,7 @@ class ContainerFlowGenerationProperties(BaseModel): transportation_buffer = FloatField( default=DEFAULT_TRANSPORTATION_BUFFER, ) + + conflowgen_version = CharField( + default=__version__ + ) diff --git a/conflowgen/application/reports/container_flow_statistics_report.py b/conflowgen/application/reports/container_flow_statistics_report.py index b673610a..9de3ffba 100644 --- a/conflowgen/application/reports/container_flow_statistics_report.py +++ b/conflowgen/application/reports/container_flow_statistics_report.py @@ -4,6 +4,8 @@ import statistics from typing import List, Type, Dict +from conflowgen.application.services.vehicle_capacity_manager import VehicleCapacityManager +from conflowgen.descriptive_datatypes import FlowDirection from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport from conflowgen.domain_models.repositories.large_scheduled_vehicle_repository import LargeScheduledVehicleRepository from conflowgen.domain_models.vehicle import AbstractLargeScheduledVehicle, LargeScheduledVehicle @@ -12,6 +14,7 @@ class ContainerFlowStatisticsReport: def __init__(self, transportation_buffer=None): self.large_scheduled_vehicle_repository = LargeScheduledVehicleRepository() + self.vehicle_capacity_manager = VehicleCapacityManager() self.logger = logging.getLogger("conflowgen") self.free_capacity_inbound_statistics = {} self.free_capacity_outbound_statistics = {} @@ -19,7 +22,7 @@ def __init__(self, transportation_buffer=None): self.set_transportation_buffer(transportation_buffer=transportation_buffer) def set_transportation_buffer(self, transportation_buffer: float) -> None: - self.large_scheduled_vehicle_repository.set_transportation_buffer(transportation_buffer) + self.vehicle_capacity_manager.set_transportation_buffer(transportation_buffer) self.logger.info(f"Use transportation buffer of {transportation_buffer} for reporting statistics.") def generate(self) -> None: @@ -30,7 +33,7 @@ def generate(self) -> None: self._generate_free_capacity_statistics(vehicles_of_types) def _generate_free_capacity_statistics(self, vehicles_of_types): - buffer_factor = 1 + self.large_scheduled_vehicle_repository.transportation_buffer + buffer_factor = 1 + self.vehicle_capacity_manager.vehicle_container_volume_calculator.transportation_buffer free_capacities_inbound = {} free_capacities_outbound = {} vehicle_type: ModeOfTransport @@ -40,11 +43,12 @@ def _generate_free_capacity_statistics(self, vehicles_of_types): # noinspection PyTypeChecker large_scheduled_vehicle: LargeScheduledVehicle = vehicle.large_scheduled_vehicle - free_capacity_inbound = self.large_scheduled_vehicle_repository.get_free_capacity_for_inbound_journey( + free_capacity_inbound = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( vehicle ) - free_capacity_outbound = self.large_scheduled_vehicle_repository.get_free_capacity_for_outbound_journey( - vehicle + free_capacity_outbound = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + vehicle, + FlowDirection.undefined ) assert free_capacity_inbound <= large_scheduled_vehicle.capacity_in_teu, \ f"A vehicle can only load at maximum its capacity, but for vehicle {vehicle} the free capacity " \ @@ -55,11 +59,11 @@ def _generate_free_capacity_statistics(self, vehicles_of_types): f"of {free_capacity_outbound} for outbound does not match with the capacity of the vehicle of " \ f"{large_scheduled_vehicle.capacity_in_teu} TEU" - assert (free_capacity_inbound <= large_scheduled_vehicle.moved_capacity), \ + assert (free_capacity_inbound <= large_scheduled_vehicle.inbound_container_volume), \ f"A vehicle must not exceed its moved capacity, but for vehicle {vehicle} the free " \ f"capacity of {free_capacity_inbound} TEU for inbound does not match with the moved capacity " \ - f"of {large_scheduled_vehicle.moved_capacity}" - moved_capacity_with_outbound_buffer = (large_scheduled_vehicle.moved_capacity * buffer_factor) + f"of {large_scheduled_vehicle.inbound_container_volume}" + moved_capacity_with_outbound_buffer = (large_scheduled_vehicle.inbound_container_volume * buffer_factor) assert (free_capacity_outbound <= moved_capacity_with_outbound_buffer), \ f"A vehicle must not exceed its transportation buffer, but for vehicle {vehicle} the free " \ f"capacity of {free_capacity_outbound} for outbound does not match with the moved capacity " \ diff --git a/conflowgen/application/services/inbound_and_outbound_vehicle_capacity_calculator_service.py b/conflowgen/application/services/inbound_and_outbound_vehicle_capacity_calculator_service.py index 5b5583fe..5d75e616 100644 --- a/conflowgen/application/services/inbound_and_outbound_vehicle_capacity_calculator_service.py +++ b/conflowgen/application/services/inbound_and_outbound_vehicle_capacity_calculator_service.py @@ -62,6 +62,7 @@ def get_inbound_capacity_of_vehicles( at_least_one_schedule_exists: bool = False + schedule: Schedule for schedule in Schedule.select(): at_least_one_schedule_exists = True arrivals = create_arrivals_within_time_range( @@ -72,7 +73,7 @@ def get_inbound_capacity_of_vehicles( schedule.vehicle_arrives_at_time ) moved_inbound_volumes = (len(arrivals) # number of vehicles that are planned - * schedule.average_moved_capacity) # moved TEU capacity of each vehicle + * schedule.average_inbound_container_volume) # moved TEU capacity of each vehicle inbound_container_volume_in_teu[schedule.vehicle_type] += moved_inbound_volumes inbound_container_volume_in_containers[schedule.vehicle_type] += moved_inbound_volumes / \ ContainerLengthDistributionRepository.get_teu_factor() @@ -120,11 +121,15 @@ def get_outbound_capacity_of_vehicles(start_date, end_date, transportation_buffe schedule: Schedule for schedule in Schedule.select(): - assert schedule.average_moved_capacity <= schedule.average_vehicle_capacity, \ - "A vehicle cannot move a larger amount of containers (in TEU) than its capacity, " \ - f"the input data is malformed. Schedule '{schedule.service_name}' of vehicle type " \ - f"{schedule.vehicle_type} has an average moved capacity of {schedule.average_moved_capacity} but an " \ - f"averaged vehicle capacity of {schedule.average_vehicle_capacity}." + assert \ + schedule.average_inbound_container_volume <= schedule.average_vehicle_capacity, \ + ( + "A vehicle cannot move a larger amount of containers (in TEU) than its capacity, " + f"the input data is malformed. Schedule '{schedule.service_name}' of vehicle type " + f"{schedule.vehicle_type} has an average moved capacity of " + f"{schedule.average_inbound_container_volume} but an averaged vehicle capacity of " + f"{schedule.average_vehicle_capacity}." + ) arrivals = create_arrivals_within_time_range( start_date, @@ -135,14 +140,14 @@ def get_outbound_capacity_of_vehicles(start_date, end_date, transportation_buffe ) # If all container flows are balanced, only the average moved capacity is required - container_volume_moved_by_vessels_in_teu = len(arrivals) * schedule.average_moved_capacity + container_volume_moved_by_vessels_in_teu = len(arrivals) * schedule.average_inbound_container_volume outbound_used_capacity_in_teu[schedule.vehicle_type] += container_volume_moved_by_vessels_in_teu outbound_used_containers[schedule.vehicle_type] += container_volume_moved_by_vessels_in_teu / \ ContainerLengthDistributionRepository.get_teu_factor() # If there are unbalanced container flows, a vehicle departs with more containers than it delivered maximum_capacity_of_vehicle_in_teu = min( - schedule.average_moved_capacity * (1 + transportation_buffer), + schedule.average_inbound_container_volume * (1 + transportation_buffer), schedule.average_vehicle_capacity ) total_maximum_capacity_moved_by_vessel = len(arrivals) * maximum_capacity_of_vehicle_in_teu diff --git a/conflowgen/application/services/vehicle_capacity_manager.py b/conflowgen/application/services/vehicle_capacity_manager.py new file mode 100644 index 00000000..23b08a36 --- /dev/null +++ b/conflowgen/application/services/vehicle_capacity_manager.py @@ -0,0 +1,212 @@ +import datetime +import logging +import typing +from typing import Dict, Callable, Type + +from conflowgen.application.services.vehicle_container_volume_calculator import VehicleContainerVolumeCalculator +from conflowgen.descriptive_datatypes import FlowDirection +from conflowgen.domain_models.container import Container +from conflowgen.domain_models.data_types.container_length import ContainerLength +from conflowgen.domain_models.vehicle import LargeScheduledVehicle, AbstractLargeScheduledVehicle + + +class VehicleCapacityManager: + ignored_capacity = ContainerLength.get_teu_factor(ContainerLength.other) + + def __init__(self): + self.vehicle_container_volume_calculator = VehicleContainerVolumeCalculator() + self.occupied_capacity_for_outbound_journey_buffer: ( + Dict)[Type[AbstractLargeScheduledVehicle], float] = {} + self.occupied_capacity_for_inbound_journey_buffer: ( + Dict)[Type[AbstractLargeScheduledVehicle], float] = {} + self.logger = logging.getLogger("conflowgen") + + def set_transportation_buffer(self, transportation_buffer: float): + self.vehicle_container_volume_calculator.set_transportation_buffer(transportation_buffer) + + def set_ramp_up_and_down_times( + self, + ramp_up_period_end: typing.Optional[datetime.datetime] = None, + ramp_down_period_start: typing.Optional[datetime.datetime] = None, + ) -> None: + self.vehicle_container_volume_calculator.set_ramp_up_and_down_times( + ramp_up_period_end=ramp_up_period_end, + ramp_down_period_start=ramp_down_period_start, + ) + + def reset_cache(self): + self.occupied_capacity_for_inbound_journey_buffer = {} + self.occupied_capacity_for_outbound_journey_buffer = {} + + def block_capacity_for_inbound_journey( + self, + vehicle: Type[AbstractLargeScheduledVehicle], + container: Container + ) -> bool: + assert vehicle in self.occupied_capacity_for_inbound_journey_buffer, \ + "First .get_free_capacity_for_inbound_journey(vehicle) must be invoked" + + usable_vessel_capacity = self.vehicle_container_volume_calculator. \ + get_transported_container_volume_on_inbound_journey(vehicle) + + occupied_capacity_in_teu = self.occupied_capacity_for_inbound_journey_buffer[vehicle] + used_capacity_in_teu = ContainerLength.get_teu_factor(container_length=container.length) + new_occupied_capacity_in_teu = occupied_capacity_in_teu + used_capacity_in_teu + + new_free_capacity_in_teu = usable_vessel_capacity - new_occupied_capacity_in_teu + assert \ + new_free_capacity_in_teu >= 0, \ + ( + f"vehicle {vehicle} is overloaded, " + f"usable_vessel_capacity: {usable_vessel_capacity}, " + f"occupied_capacity_in_teu: {occupied_capacity_in_teu}, " + f"used_capacity_in_teu: {used_capacity_in_teu}, " + f"new_free_capacity_in_teu: {new_free_capacity_in_teu}" + ) + + self.occupied_capacity_for_inbound_journey_buffer[vehicle] = new_occupied_capacity_in_teu + vehicle_capacity_is_exhausted = new_free_capacity_in_teu < self.ignored_capacity + return vehicle_capacity_is_exhausted + + def block_capacity_for_outbound_journey( + self, + vehicle: Type[AbstractLargeScheduledVehicle], + container: Container + ) -> bool: + assert vehicle in self.occupied_capacity_for_outbound_journey_buffer, \ + "First .get_free_capacity_for_outbound_journey(vehicle) must be invoked" + + scaled_moved_container_volume, unscaled_moved_container_volume = self.vehicle_container_volume_calculator. \ + get_maximum_transported_container_volume_on_outbound_journey(vehicle, container.flow_direction) + + # calculate new free capacity + occupied_capacity_in_teu = self.occupied_capacity_for_outbound_journey_buffer[vehicle] + used_capacity_in_teu = ContainerLength.get_teu_factor(container_length=container.length) + new_occupied_capacity_in_teu = occupied_capacity_in_teu + used_capacity_in_teu + + new_scaled_free_capacity_in_teu = scaled_moved_container_volume - new_occupied_capacity_in_teu + + new_unscaled_free_capacity_in_teu = unscaled_moved_container_volume - new_occupied_capacity_in_teu + assert \ + new_unscaled_free_capacity_in_teu >= 0, \ + ( + f"vehicle {vehicle} is overloaded, " + f"scaled_moved_container_volume: {scaled_moved_container_volume}, " + f"unscaled_moved_container_volume: {unscaled_moved_container_volume}, " + f"occupied_capacity_in_teu: {occupied_capacity_in_teu}, " + f"used_capacity_in_teu: {used_capacity_in_teu}, " + f"new_scaled_free_capacity_in_teu: {new_scaled_free_capacity_in_teu} " + f"new_unscaled_free_capacity_in_teu: {new_unscaled_free_capacity_in_teu}" + ) + + self.occupied_capacity_for_outbound_journey_buffer[vehicle] = new_occupied_capacity_in_teu + + space_is_exhausted = (new_scaled_free_capacity_in_teu <= self.ignored_capacity) + return space_is_exhausted + + def get_free_capacity_for_inbound_journey(self, vehicle: Type[AbstractLargeScheduledVehicle]) -> float: + """ + Get the free capacity for the inbound journey on a vehicle that moves according to a schedule in TEU. + During the ramp-down period (if existent), all inbound traffic is scaled down, no matter what. + """ + inbound_container_volume = self.vehicle_container_volume_calculator \ + .get_transported_container_volume_on_inbound_journey(vehicle) + + if vehicle in self.occupied_capacity_for_inbound_journey_buffer: + occupied_capacity_in_teu = self.occupied_capacity_for_inbound_journey_buffer[vehicle] + else: + occupied_capacity_in_teu = self._get_occupied_capacity_in_teu( + vehicle=vehicle, + container_counter=self._get_number_containers_for_inbound_journey, + ) + self.occupied_capacity_for_inbound_journey_buffer[vehicle] = occupied_capacity_in_teu + + free_capacity_in_teu = inbound_container_volume - occupied_capacity_in_teu + return free_capacity_in_teu + + def get_free_capacity_for_outbound_journey( + self, + vehicle: Type[AbstractLargeScheduledVehicle], + flow_direction: FlowDirection + ) -> float: + """ + Get the free capacity for the outbound journey on a vehicle that moves according to a schedule in TEU. + During the ramp-up period (if existent), all outbound traffic that constitutes transshipment, is scaled down. + """ + + # During the ramp-up period, the container volume is reduced at this stage + maximum_transported_container_volume, unscaled_moved_container_volume = \ + self.vehicle_container_volume_calculator.get_maximum_transported_container_volume_on_outbound_journey( + vehicle, flow_direction + ) + + if vehicle in self.occupied_capacity_for_outbound_journey_buffer: + occupied_capacity_in_teu = self.occupied_capacity_for_outbound_journey_buffer[vehicle] + else: + occupied_capacity_in_teu = self._get_occupied_capacity_in_teu( + vehicle=vehicle, + container_counter=self._get_number_containers_for_outbound_journey, + ) + self.occupied_capacity_for_outbound_journey_buffer[vehicle] = occupied_capacity_in_teu + + assert \ + unscaled_moved_container_volume - occupied_capacity_in_teu >= 0, \ + ( + f"vehicle {vehicle} is overloaded, " + f"maximum_transported_container_volume: {maximum_transported_container_volume}, " + f"unscaled_moved_container_volume: {unscaled_moved_container_volume}, " + f"occupied_capacity_in_teu: {occupied_capacity_in_teu}" + ) + + free_capacity = max(maximum_transported_container_volume - occupied_capacity_in_teu, 0) + + return free_capacity + + @staticmethod + def _get_occupied_capacity_in_teu( + vehicle: Type[AbstractLargeScheduledVehicle], + container_counter: Callable[[Type[AbstractLargeScheduledVehicle], ContainerLength], int], + ) -> (float, float): + loaded_20_foot_containers = container_counter(vehicle, ContainerLength.twenty_feet) + loaded_40_foot_containers = container_counter(vehicle, ContainerLength.forty_feet) + loaded_45_foot_containers = container_counter(vehicle, ContainerLength.forty_five_feet) + loaded_other_containers = container_counter(vehicle, ContainerLength.other) + occupied_capacity = ( + loaded_20_foot_containers * ContainerLength.get_teu_factor(ContainerLength.twenty_feet) + + loaded_40_foot_containers * ContainerLength.get_teu_factor(ContainerLength.forty_feet) + + loaded_45_foot_containers * ContainerLength.get_teu_factor(ContainerLength.forty_five_feet) + + loaded_other_containers * ContainerLength.get_teu_factor(ContainerLength.other) + ) + return occupied_capacity + + @classmethod + def _get_number_containers_for_outbound_journey( + cls, + vehicle: Type[AbstractLargeScheduledVehicle], + container_length: ContainerLength + ) -> int: + """Returns the number of containers on a specific vehicle of a specific container length that are picked up by + the vehicle""" + # noinspection PyTypeChecker + large_scheduled_vehicle: LargeScheduledVehicle = vehicle.large_scheduled_vehicle + number_loaded_containers = Container.select().where( + (Container.picked_up_by_large_scheduled_vehicle == large_scheduled_vehicle) + & (Container.length == container_length) + ).count() + return number_loaded_containers + + @classmethod + def _get_number_containers_for_inbound_journey( + cls, + vehicle: Type[AbstractLargeScheduledVehicle], + container_length: ContainerLength + ) -> int: + """Returns the number of containers on a specific vehicle of a specific container length that are delivered by + the vehicle""" + + large_scheduled_vehicle: LargeScheduledVehicle = vehicle.large_scheduled_vehicle + number_loaded_containers = Container.select().where( + (Container.delivered_by_large_scheduled_vehicle == large_scheduled_vehicle) + & (Container.length == container_length) + ).count() + return number_loaded_containers diff --git a/conflowgen/application/services/vehicle_container_volume_calculator.py b/conflowgen/application/services/vehicle_container_volume_calculator.py new file mode 100644 index 00000000..ab9a1f12 --- /dev/null +++ b/conflowgen/application/services/vehicle_container_volume_calculator.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +import datetime +import logging +import typing + +from conflowgen.descriptive_datatypes import FlowDirection +from conflowgen.domain_models.vehicle import LargeScheduledVehicle, AbstractLargeScheduledVehicle + + +class VehicleContainerVolumeCalculator: + + downscale_factor_during_ramp_up_period_for_outbound_transshipment = 0.2 + + downscale_factor_during_ramp_down_period_for_inbound_all_kinds = 0.2 + + def __init__(self): + self.transportation_buffer = None + self.ramp_up_period_end = None + self.ramp_down_period_start = None + self.logger = logging.getLogger("conflowgen") + + def set_transportation_buffer( + self, + transportation_buffer: float + ) -> None: + 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] = None, + ramp_down_period_start: typing.Optional[datetime.datetime] = None + ) -> None: + self.ramp_up_period_end = ramp_up_period_end + self.ramp_down_period_start = ramp_down_period_start + + def get_transported_container_volume_on_inbound_journey( + self, + large_scheduled_vehicle: LargeScheduledVehicle | typing.Type[AbstractLargeScheduledVehicle], + ) -> float: + """ + + Args: + large_scheduled_vehicle: The vehicle for which the transported container volume is calculated for + + Returns: + The container volume in TEU that are transported by this vehicle on its inbound journey. The volume is + scaled down during the ramp-down period if present. + """ + + # auto-cast vehicles to their LargeScheduledVehicle reference + if hasattr(large_scheduled_vehicle, "large_scheduled_vehicle"): + large_scheduled_vehicle = large_scheduled_vehicle.large_scheduled_vehicle + + # This is our default + moved_container_volume = large_scheduled_vehicle.inbound_container_volume + + if self.ramp_down_period_start is not None: + arrival_time: datetime.datetime = large_scheduled_vehicle.scheduled_arrival + if arrival_time >= self.ramp_down_period_start: + moved_container_volume *= self.downscale_factor_during_ramp_down_period_for_inbound_all_kinds + + return moved_container_volume + + def get_maximum_transported_container_volume_on_outbound_journey( + self, + large_scheduled_vehicle: LargeScheduledVehicle | typing.Type[AbstractLargeScheduledVehicle], + flow_direction: FlowDirection, + ) -> (float, float): + """ + + Args: + large_scheduled_vehicle: The vehicle for which the expected transported container volume is calculated for + flow_direction: The flow direction to consider. + + Returns: + The container volume in TEU that are transported by this vehicle on its outbound journey. The volume is + scaled down during the ramp-down period if present for transshipment containers. + """ + + assert self.transportation_buffer is not None, "Transportation buffer must be set" + assert -1 < self.transportation_buffer, "Transportation buffer must be larger than -1" + + # auto-cast vehicles to their LargeScheduledVehicle reference + if hasattr(large_scheduled_vehicle, "large_scheduled_vehicle"): + large_scheduled_vehicle = large_scheduled_vehicle.large_scheduled_vehicle + + # this is our default + unscaled_moved_container_volume = self._get_maximum_outbound_capacity_in_teu(large_scheduled_vehicle) + scaled_moved_container_volume = unscaled_moved_container_volume + + if self.ramp_up_period_end is not None and flow_direction == FlowDirection.transshipment_flow: + arrival_time: datetime.datetime = large_scheduled_vehicle.scheduled_arrival + if arrival_time <= self.ramp_up_period_end: + scaled_moved_container_volume = ( + unscaled_moved_container_volume + * self.downscale_factor_during_ramp_up_period_for_outbound_transshipment + ) + + return scaled_moved_container_volume, unscaled_moved_container_volume + + def _get_maximum_outbound_capacity_in_teu( + self, + large_scheduled_vehicle: LargeScheduledVehicle + ) -> int: + assert self.transportation_buffer is not None, "Please first set the transportation buffer!" + expected_outbound_capacity_including_transportation_buffer = \ + large_scheduled_vehicle.inbound_container_volume * (1 + self.transportation_buffer) + maximum_capacity_of_vehicle = large_scheduled_vehicle.capacity_in_teu + maximum_outbound_capacity_in_teu = min( + expected_outbound_capacity_including_transportation_buffer, + maximum_capacity_of_vehicle + ) + return maximum_outbound_capacity_in_teu diff --git a/conflowgen/descriptive_datatypes/__init__.py b/conflowgen/descriptive_datatypes/__init__.py index c8d25ad0..06430e32 100644 --- a/conflowgen/descriptive_datatypes/__init__.py +++ b/conflowgen/descriptive_datatypes/__init__.py @@ -1,6 +1,7 @@ from __future__ import annotations import datetime +import enum import typing from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport @@ -92,13 +93,16 @@ class VehicleIdentifier(typing.NamedTuple): A vehicle identifier is a composition of the vehicle type, its service name, and the actual vehicle name """ + #: The vehicle identifier as it is used in the CSV export. + id: typing.Optional[int] + #: The vehicle type, e.g., 'deep_sea_vessel' or 'truck'. mode_of_transport: ModeOfTransport #: The service name, such as the name of the container service the vessel operates in. Not set for trucks. service_name: typing.Optional[str] - #: The name of the vehicle if given. + #: The name of the vehicle if given. Not set for trucks. vehicle_name: typing.Optional[str] #: The time of arrival of the vehicle at the terminal. @@ -127,3 +131,22 @@ class ContainersTransportedByTruck(typing.NamedTuple): #: The number of containers moved on the outbound journey outbound: float + + +class FlowDirection(enum.Enum): + """ + Represents the flow direction based on the terminology of + *Handbook of Terminal Planning*, edited by Jürgen W. Böse (https://link.springer.com/book/10.1007/978-3-030-39990-0) + """ + + import_flow = "import" + + export_flow = "export" + + transshipment_flow = "transshipment" + + undefined = "undefined" + + def __str__(self) -> str: + # noinspection PyTypeChecker + return f"{self.value}" diff --git a/conflowgen/domain_models/container.py b/conflowgen/domain_models/container.py index 043c9ad4..9da6453b 100644 --- a/conflowgen/domain_models/container.py +++ b/conflowgen/domain_models/container.py @@ -14,6 +14,7 @@ from .vehicle import LargeScheduledVehicle from .vehicle import Truck from .data_types.storage_requirement import StorageRequirement +from ..descriptive_datatypes import FlowDirection from ..domain_models.data_types.mode_of_transport import ModeOfTransport @@ -116,6 +117,19 @@ class Container(BaseModel): def occupied_teu(self) -> float: return CONTAINER_LENGTH_TO_OCCUPIED_TEU[self.length] + @property + def flow_direction(self) -> FlowDirection: + if (self.delivered_by in [ModeOfTransport.truck, ModeOfTransport.train, ModeOfTransport.barge] + and self.picked_up_by in [ModeOfTransport.feeder, ModeOfTransport.deep_sea_vessel]): + return FlowDirection.export_flow + if (self.picked_up_by in [ModeOfTransport.truck, ModeOfTransport.train, ModeOfTransport.barge] + and self.delivered_by in [ModeOfTransport.feeder, ModeOfTransport.deep_sea_vessel]): + return FlowDirection.import_flow + if (self.picked_up_by in [ModeOfTransport.feeder, ModeOfTransport.deep_sea_vessel] + and self.delivered_by in [ModeOfTransport.feeder, ModeOfTransport.deep_sea_vessel]): + return FlowDirection.transshipment_flow + return FlowDirection.undefined + def get_arrival_time(self) -> datetime.datetime: if self.cached_arrival_time is not None: diff --git a/conflowgen/domain_models/factories/container_factory.py b/conflowgen/domain_models/factories/container_factory.py index bd3aab8a..ad241bf5 100644 --- a/conflowgen/domain_models/factories/container_factory.py +++ b/conflowgen/domain_models/factories/container_factory.py @@ -1,8 +1,11 @@ from __future__ import annotations +import datetime import math +import typing from typing import Dict, MutableSequence, Sequence, Type +from conflowgen.application.services.vehicle_capacity_manager import VehicleCapacityManager from conflowgen.domain_models.container import Container from conflowgen.domain_models.distribution_repositories.container_length_distribution_repository import \ ContainerLengthDistributionRepository @@ -15,7 +18,6 @@ from conflowgen.domain_models.data_types.container_length import ContainerLength from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport from conflowgen.domain_models.data_types.storage_requirement import StorageRequirement -from conflowgen.domain_models.repositories.large_scheduled_vehicle_repository import LargeScheduledVehicleRepository from conflowgen.domain_models.vehicle import AbstractLargeScheduledVehicle, LargeScheduledVehicle from conflowgen.tools.distribution_approximator import DistributionApproximator from conflowgen.application.repositories.random_seed_store_repository import get_initialised_random_object @@ -34,7 +36,17 @@ def __init__(self): self.container_length_distribution: dict[ContainerLength, float] | None = None self.container_weight_distribution: dict[ContainerLength, dict[int, float]] | None = None self.storage_requirement_distribution: dict[ContainerLength, dict[StorageRequirement, float]] | None = None - self.large_scheduled_vehicle_repository = LargeScheduledVehicleRepository() + self.vehicle_capacity_manager = VehicleCapacityManager() + + def set_ramp_up_and_down_times( + self, + ramp_up_period_end: typing.Optional[datetime.datetime], + ramp_down_period_start: typing.Optional[datetime.datetime], + ) -> None: + self.vehicle_capacity_manager.set_ramp_up_and_down_times( + ramp_up_period_end=ramp_up_period_end, + ramp_down_period_start=ramp_down_period_start + ) def reload_distributions(self): """The user might change the distributions at any time, so reload them at a meaningful point of time!""" @@ -51,7 +63,7 @@ def create_containers_for_large_scheduled_vehicle( Creates all containers a large vehicle delivers to a terminal. """ - self.large_scheduled_vehicle_repository.reset_cache() + self.vehicle_capacity_manager.reset_cache() created_containers: MutableSequence[Container] = [] @@ -60,7 +72,7 @@ def create_containers_for_large_scheduled_vehicle( # noinspection PyTypeChecker large_scheduled_vehicle: LargeScheduledVehicle = large_scheduled_vehicle_as_subtype.large_scheduled_vehicle - free_capacity_in_teu = self.large_scheduled_vehicle_repository.get_free_capacity_for_inbound_journey( + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( large_scheduled_vehicle_as_subtype ) @@ -68,22 +80,25 @@ def create_containers_for_large_scheduled_vehicle( maximum_number_of_containers = int(math.ceil(free_capacity_in_teu)) self._load_distribution_approximators(maximum_number_of_containers, delivered_by) - while (self.large_scheduled_vehicle_repository.get_free_capacity_for_inbound_journey( - large_scheduled_vehicle_as_subtype - ) > self.ignored_capacity): + while free_capacity_in_teu > self.ignored_capacity: container = self._create_single_container_for_large_scheduled_vehicle( delivered_by_large_scheduled_vehicle_as_subtype=large_scheduled_vehicle_as_subtype ) created_containers.append(container) - is_exhausted = self.large_scheduled_vehicle_repository.block_capacity_for_inbound_journey( + is_exhausted = self.vehicle_capacity_manager.block_capacity_for_inbound_journey( vehicle=large_scheduled_vehicle_as_subtype, container=container ) if is_exhausted and not large_scheduled_vehicle.capacity_exhausted_while_determining_onward_transportation: large_scheduled_vehicle.capacity_exhausted_while_determining_onward_transportation = True large_scheduled_vehicle.save() + break + + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( + large_scheduled_vehicle_as_subtype + ) - free_capacity = self.large_scheduled_vehicle_repository.get_free_capacity_for_inbound_journey( + free_capacity = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( large_scheduled_vehicle_as_subtype ) diff --git a/conflowgen/domain_models/factories/fleet_factory.py b/conflowgen/domain_models/factories/fleet_factory.py index f721fa1d..de873adb 100644 --- a/conflowgen/domain_models/factories/fleet_factory.py +++ b/conflowgen/domain_models/factories/fleet_factory.py @@ -77,12 +77,12 @@ def create_feeder_fleet( ) for i, arrival in enumerate(arrivals): - moved_capacity = schedule.average_moved_capacity # here we can add randomness later + inbound_container_volume = schedule.average_inbound_container_volume # here we can add randomness later vehicle_name = f"{i + 1}" feeder = self.vehicle_factory.create_feeder( vehicle_name=vehicle_name, capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=moved_capacity, + inbound_container_volume=inbound_container_volume, scheduled_arrival=arrival, schedule=schedule ) @@ -112,13 +112,13 @@ def create_deep_sea_vessel_fleet( ) for i, arrival in enumerate(arrivals): - moved_capacity = schedule.average_moved_capacity # here we can add randomness later + inbound_container_volume = schedule.average_inbound_container_volume # here we can add randomness later vehicle_name = f"{i + 1}" deep_sea_vessel = self.vehicle_factory.create_deep_sea_vessel( vehicle_name=vehicle_name, capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=moved_capacity, + inbound_container_volume=inbound_container_volume, scheduled_arrival=arrival, schedule=schedule ) @@ -146,13 +146,13 @@ def create_train_fleet( ) for i, arrival in enumerate(arrivals): - moved_capacity = schedule.average_moved_capacity # here we can add randomness later + inbound_container_volume = schedule.average_inbound_container_volume # here we can add randomness later vehicle_name = f"{i + 1}" train = self.vehicle_factory.create_train( vehicle_name=vehicle_name, capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=moved_capacity, + inbound_container_volume=inbound_container_volume, scheduled_arrival=arrival, schedule=schedule ) @@ -180,13 +180,13 @@ def create_barge_fleet( ) for i, arrival in enumerate(arrivals): - moved_capacity = schedule.average_moved_capacity # here we can add randomness later + inbound_container_volume = schedule.average_inbound_container_volume # here we can add randomness later vehicle_name = f"{i + 1}" barge = self.vehicle_factory.create_barge( vehicle_name=vehicle_name, capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=moved_capacity, + inbound_container_volume=inbound_container_volume, scheduled_arrival=arrival, schedule=schedule ) diff --git a/conflowgen/domain_models/factories/schedule_factory.py b/conflowgen/domain_models/factories/schedule_factory.py index 1670c3f4..8a41f0da 100644 --- a/conflowgen/domain_models/factories/schedule_factory.py +++ b/conflowgen/domain_models/factories/schedule_factory.py @@ -19,7 +19,7 @@ def add_schedule( vehicle_arrives_at: datetime.date, vehicle_arrives_at_time: datetime.time, average_vehicle_capacity: int, - average_moved_capacity: int, + average_inbound_container_volume: int, next_destinations: Optional[List[Tuple[str, float]]], vehicle_arrives_every_k_days: Optional[int] = None ) -> None: @@ -31,7 +31,7 @@ def add_schedule( vehicle_arrives_at: date of day vehicle_arrives_at_time: time of the day average_vehicle_capacity: in TEU - average_moved_capacity: in TEU + average_inbound_container_volume: in TEU next_destinations: distribution vehicle_arrives_every_k_days: Be aware of special meaning of None and -1! """ @@ -44,7 +44,7 @@ def add_schedule( vehicle_arrives_at=vehicle_arrives_at, vehicle_arrives_at_time=vehicle_arrives_at_time, average_vehicle_capacity=average_vehicle_capacity, - average_moved_capacity=average_moved_capacity + average_inbound_container_volume=average_inbound_container_volume ) # if it is None, use the default set in peewee, otherwise overwrite if vehicle_arrives_every_k_days is not None: diff --git a/conflowgen/domain_models/factories/vehicle_factory.py b/conflowgen/domain_models/factories/vehicle_factory.py index d675c21d..cba2da08 100644 --- a/conflowgen/domain_models/factories/vehicle_factory.py +++ b/conflowgen/domain_models/factories/vehicle_factory.py @@ -78,7 +78,7 @@ def create_truck( def _create_large_vehicle( self, capacity_in_teu: int, - moved_capacity: int, + inbound_container_volume: int, scheduled_arrival: datetime.datetime, realized_arrival: datetime.datetime, schedule: Schedule, @@ -91,13 +91,13 @@ def _create_large_vehicle( if capacity_in_teu < 0: raise UnrealisticValuesException(f"Vehicle capacity must be positive but it was {capacity_in_teu}") - if moved_capacity < 0: - raise UnrealisticValuesException(f"Vehicle must move positive amount but it was {moved_capacity}") + if inbound_container_volume < 0: + raise UnrealisticValuesException(f"Vehicle must move positive amount but it was {inbound_container_volume}") - if moved_capacity > capacity_in_teu: + if inbound_container_volume > capacity_in_teu: raise UnrealisticValuesException( f"Vehicle can't move more than its capacity but for the vehicle with an overall capacity of " - f"{capacity_in_teu} the moved capacity was set to {moved_capacity}" + f"{capacity_in_teu} the moved capacity was set to {inbound_container_volume}" ) if vehicle_name is None: @@ -106,7 +106,7 @@ def _create_large_vehicle( lsv = LargeScheduledVehicle.create( vehicle_name=vehicle_name, capacity_in_teu=capacity_in_teu, - moved_capacity=moved_capacity, + inbound_container_volume=inbound_container_volume, scheduled_arrival=scheduled_arrival, realized_arrival=realized_arrival, schedule=schedule @@ -116,7 +116,7 @@ def _create_large_vehicle( def create_feeder( self, capacity_in_teu: int, - moved_capacity: int, + inbound_container_volume: int, scheduled_arrival: datetime.datetime, schedule: Schedule, vehicle_name: Optional[str] = None @@ -126,7 +126,7 @@ def create_feeder( lsv = self._create_large_vehicle( vehicle_name=vehicle_name, capacity_in_teu=capacity_in_teu, - moved_capacity=moved_capacity, + inbound_container_volume=inbound_container_volume, scheduled_arrival=scheduled_arrival, realized_arrival=scheduled_arrival, schedule=schedule @@ -139,7 +139,7 @@ def create_feeder( def create_deep_sea_vessel( self, capacity_in_teu: int, - moved_capacity: int, + inbound_container_volume: int, scheduled_arrival: datetime.datetime, schedule: Schedule, vehicle_name: Optional[str] = None @@ -149,7 +149,7 @@ def create_deep_sea_vessel( lsv = self._create_large_vehicle( vehicle_name=vehicle_name, capacity_in_teu=capacity_in_teu, - moved_capacity=moved_capacity, + inbound_container_volume=inbound_container_volume, scheduled_arrival=scheduled_arrival, realized_arrival=scheduled_arrival, schedule=schedule @@ -162,7 +162,7 @@ def create_deep_sea_vessel( def create_train( self, capacity_in_teu: int, - moved_capacity: int, + inbound_container_volume: int, scheduled_arrival: datetime.datetime, schedule: Schedule, vehicle_name: Optional[str] = None @@ -172,7 +172,7 @@ def create_train( lsv = self._create_large_vehicle( vehicle_name=vehicle_name, capacity_in_teu=capacity_in_teu, - moved_capacity=moved_capacity, + inbound_container_volume=inbound_container_volume, scheduled_arrival=scheduled_arrival, realized_arrival=scheduled_arrival, schedule=schedule @@ -185,7 +185,7 @@ def create_train( def create_barge( self, capacity_in_teu: int, - moved_capacity: int, + inbound_container_volume: int, scheduled_arrival: datetime.datetime, schedule: Schedule, vehicle_name: Optional[str] = None @@ -195,7 +195,7 @@ def create_barge( lsv = self._create_large_vehicle( vehicle_name=vehicle_name, capacity_in_teu=capacity_in_teu, - moved_capacity=moved_capacity, + inbound_container_volume=inbound_container_volume, scheduled_arrival=scheduled_arrival, realized_arrival=scheduled_arrival, schedule=schedule diff --git a/conflowgen/domain_models/large_vehicle_schedule.py b/conflowgen/domain_models/large_vehicle_schedule.py index b54ccd67..d51bfb9a 100644 --- a/conflowgen/domain_models/large_vehicle_schedule.py +++ b/conflowgen/domain_models/large_vehicle_schedule.py @@ -26,7 +26,7 @@ class Schedule(BaseModel): "This determines the number of ship-to-shore gantry cranes that can serve the vessel " "or the length of the train that must be served by portal cranes in the subsequent model." ) - average_moved_capacity = IntegerField( + average_inbound_container_volume = IntegerField( null=False, help_text="All vehicles moving according to this schedule move approx. this amount of TEU. " "The actual amount of moved TEU might deviate if necessary to realize the provided modal split." diff --git a/conflowgen/domain_models/repositories/large_scheduled_vehicle_repository.py b/conflowgen/domain_models/repositories/large_scheduled_vehicle_repository.py index 05563689..4bc8a27f 100644 --- a/conflowgen/domain_models/repositories/large_scheduled_vehicle_repository.py +++ b/conflowgen/domain_models/repositories/large_scheduled_vehicle_repository.py @@ -1,30 +1,11 @@ -import logging -from typing import Dict, List, Callable, Type +from typing import Dict, List, Type -from conflowgen.domain_models.container import Container -from conflowgen.domain_models.data_types.container_length import ContainerLength from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport from conflowgen.domain_models.vehicle import LargeScheduledVehicle, AbstractLargeScheduledVehicle class LargeScheduledVehicleRepository: - ignored_capacity = ContainerLength.get_teu_factor(ContainerLength.other) - - def __init__(self): - self.transportation_buffer = 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") - - def set_transportation_buffer(self, transportation_buffer: float): - assert -1 < transportation_buffer - self.transportation_buffer = transportation_buffer - - def reset_cache(self): - self.free_capacity_for_outbound_journey_buffer = {} - self.free_capacity_for_inbound_journey_buffer = {} - @staticmethod def load_all_vehicles() -> Dict[ModeOfTransport, List[Type[AbstractLargeScheduledVehicle]]]: result = {} @@ -33,151 +14,3 @@ def load_all_vehicles() -> Dict[ModeOfTransport, List[Type[AbstractLargeSchedule vehicle_type) result[vehicle_type] = list(large_schedule_vehicle_as_subtype.select().join(LargeScheduledVehicle)) return result - - def block_capacity_for_inbound_journey( - self, - vehicle: Type[AbstractLargeScheduledVehicle], - container: Container - ) -> bool: - assert vehicle in self.free_capacity_for_inbound_journey_buffer, \ - "First .get_free_capacity_for_inbound_journey(vehicle) must be invoked" - - # calculate new free capacity - free_capacity_in_teu = self.free_capacity_for_inbound_journey_buffer[vehicle] - used_capacity_in_teu = ContainerLength.get_teu_factor(container_length=container.length) - new_free_capacity_in_teu = free_capacity_in_teu - used_capacity_in_teu - assert new_free_capacity_in_teu >= 0, f"vehicle {vehicle} is overloaded, " \ - f"free_capacity_in_teu: {free_capacity_in_teu}, " \ - f"used_capacity_in_teu: {used_capacity_in_teu}, " \ - f"new_free_capacity_in_teu: {new_free_capacity_in_teu}" - - self.free_capacity_for_inbound_journey_buffer[vehicle] = new_free_capacity_in_teu - vehicle_capacity_is_exhausted = new_free_capacity_in_teu < self.ignored_capacity - return vehicle_capacity_is_exhausted - - def block_capacity_for_outbound_journey( - self, - vehicle: Type[AbstractLargeScheduledVehicle], - container: Container - ) -> bool: - assert vehicle in self.free_capacity_for_outbound_journey_buffer, \ - "First .get_free_capacity_for_outbound_journey(vehicle) must be invoked" - - # calculate new free capacity - free_capacity_in_teu = self.free_capacity_for_outbound_journey_buffer[vehicle] - used_capacity_in_teu = ContainerLength.get_teu_factor(container_length=container.length) - new_free_capacity_in_teu = free_capacity_in_teu - used_capacity_in_teu - assert new_free_capacity_in_teu >= 0, f"vehicle {vehicle} is overloaded, " \ - f"free_capacity_in_teu: {free_capacity_in_teu}, " \ - f"used_capacity_in_teu: {used_capacity_in_teu}, " \ - f"new_free_capacity_in_teu: {new_free_capacity_in_teu}" - - self.free_capacity_for_outbound_journey_buffer[vehicle] = new_free_capacity_in_teu - return new_free_capacity_in_teu <= self.ignored_capacity - - # noinspection PyTypeChecker - def get_free_capacity_for_inbound_journey(self, vehicle: Type[AbstractLargeScheduledVehicle]) -> float: - """Get the free capacity for the inbound journey on a vehicle that moves according to a schedule in TEU. - """ - if vehicle in self.free_capacity_for_inbound_journey_buffer: - return self.free_capacity_for_inbound_journey_buffer[vehicle] - - large_scheduled_vehicle: LargeScheduledVehicle = vehicle.large_scheduled_vehicle - total_moved_capacity_for_inbound_transportation_in_teu = large_scheduled_vehicle.moved_capacity - 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 - ) - self.free_capacity_for_inbound_journey_buffer[vehicle] = free_capacity_in_teu - return free_capacity_in_teu - - def get_free_capacity_for_outbound_journey(self, vehicle: Type[AbstractLargeScheduledVehicle]) -> float: - """Get the free capacity for the outbound journey on a vehicle that moves according to a schedule in TEU. - """ - assert self.transportation_buffer is not None, "First set the value!" - assert -1 < self.transportation_buffer, "Must be larger than -1" - - if vehicle in self.free_capacity_for_outbound_journey_buffer: - return self.free_capacity_for_outbound_journey_buffer[vehicle] - - # noinspection PyTypeChecker - large_scheduled_vehicle: LargeScheduledVehicle = vehicle.large_scheduled_vehicle - - total_moved_capacity_for_onward_transportation_in_teu = \ - large_scheduled_vehicle.moved_capacity * (1 + self.transportation_buffer) - maximum_capacity_of_vehicle = large_scheduled_vehicle.capacity_in_teu - total_moved_capacity_for_onward_transportation_in_teu = min( - total_moved_capacity_for_onward_transportation_in_teu, - maximum_capacity_of_vehicle - ) - - 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 - ) - self.free_capacity_for_outbound_journey_buffer[vehicle] = free_capacity_in_teu - return free_capacity_in_teu - - # noinspection PyUnresolvedReferences - @staticmethod - def _get_free_capacity_in_teu( - vehicle: Type[AbstractLargeScheduledVehicle], - maximum_capacity: int, - container_counter: Callable[[Type[AbstractLargeScheduledVehicle], ContainerLength], int] - ) -> float: - loaded_20_foot_containers = container_counter(vehicle, ContainerLength.twenty_feet) - loaded_40_foot_containers = container_counter(vehicle, ContainerLength.forty_feet) - loaded_45_foot_containers = container_counter(vehicle, ContainerLength.forty_five_feet) - loaded_other_containers = container_counter(vehicle, ContainerLength.other) - free_capacity_in_teu = ( - maximum_capacity - - loaded_20_foot_containers * ContainerLength.get_teu_factor(ContainerLength.twenty_feet) - - loaded_40_foot_containers * ContainerLength.get_teu_factor(ContainerLength.forty_feet) - - loaded_45_foot_containers * ContainerLength.get_teu_factor(ContainerLength.forty_five_feet) - - loaded_other_containers * ContainerLength.get_teu_factor(ContainerLength.other) - ) - vehicle_name = vehicle.large_scheduled_vehicle.vehicle_name - assert free_capacity_in_teu >= 0, f"vehicle {vehicle} of type {vehicle.get_mode_of_transport()} with the " \ - f"name '{vehicle_name}' " \ - f"is overloaded, " \ - f"free_capacity_in_teu: {free_capacity_in_teu} with " \ - f"maximum_capacity: {maximum_capacity}, " \ - f"loaded_20_foot_containers: {loaded_20_foot_containers}, " \ - 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}" - return free_capacity_in_teu - - @classmethod - def _get_number_containers_for_outbound_journey( - cls, - vehicle: Type[AbstractLargeScheduledVehicle], - container_length: ContainerLength - ) -> int: - """Returns the number of containers on a specific vehicle of a specific container length that are picked up by - the vehicle""" - # noinspection PyTypeChecker - large_scheduled_vehicle: LargeScheduledVehicle = vehicle.large_scheduled_vehicle - number_loaded_containers = Container.select().where( - (Container.picked_up_by_large_scheduled_vehicle == large_scheduled_vehicle) - & (Container.length == container_length) - ).count() - return number_loaded_containers - - @classmethod - def _get_number_containers_for_inbound_journey( - cls, - vehicle: AbstractLargeScheduledVehicle, - container_length: ContainerLength - ) -> int: - """Returns the number of containers on a specific vehicle of a specific container length that are delivered by - the vehicle""" - - large_scheduled_vehicle: LargeScheduledVehicle = vehicle.large_scheduled_vehicle - number_loaded_containers = Container.select().where( - (Container.delivered_by_large_scheduled_vehicle == large_scheduled_vehicle) - & (Container.length == container_length) - ).count() - return number_loaded_containers diff --git a/conflowgen/domain_models/repositories/schedule_repository.py b/conflowgen/domain_models/repositories/schedule_repository.py index b45382a3..cca76684 100644 --- a/conflowgen/domain_models/repositories/schedule_repository.py +++ b/conflowgen/domain_models/repositories/schedule_repository.py @@ -1,7 +1,10 @@ import datetime +import typing from typing import List, Type import logging +from conflowgen.application.services.vehicle_capacity_manager import VehicleCapacityManager +from conflowgen.descriptive_datatypes import FlowDirection from conflowgen.domain_models.container import Container from conflowgen.domain_models.data_types.container_length import ContainerLength from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport @@ -14,16 +17,28 @@ class ScheduleRepository: def __init__(self): self.logger = logging.getLogger("conflowgen") self.large_scheduled_vehicle_repository = LargeScheduledVehicleRepository() + self.vehicle_capacity_manager = VehicleCapacityManager() def set_transportation_buffer(self, transportation_buffer: float): - self.large_scheduled_vehicle_repository.set_transportation_buffer(transportation_buffer) + self.vehicle_capacity_manager.set_transportation_buffer(transportation_buffer) + + def set_ramp_up_and_down_times( + self, + ramp_up_period_end: typing.Optional[datetime.datetime] = None, + ramp_down_period_start: typing.Optional[datetime.datetime] = None + ): + self.vehicle_capacity_manager.set_ramp_up_and_down_times( + ramp_up_period_end=ramp_up_period_end, + ramp_down_period_start=ramp_down_period_start + ) def get_departing_vehicles( self, start: datetime.datetime, end: datetime.datetime, vehicle_type: ModeOfTransport, - required_capacity: ContainerLength + required_capacity: ContainerLength, + flow_direction: FlowDirection ) -> List[Type[AbstractLargeScheduledVehicle]]: """Gets the available vehicles for the required capacity of the required type and within the time range. """ @@ -45,9 +60,11 @@ def get_departing_vehicles( vehicles_with_sufficient_capacity = [] vehicle: Type[AbstractLargeScheduledVehicle] for vehicle in vehicles: - free_capacity_in_teu = self.large_scheduled_vehicle_repository.get_free_capacity_for_outbound_journey( - vehicle - ) + free_capacity_in_teu = self.vehicle_capacity_manager.\ + get_free_capacity_for_outbound_journey( + vehicle, flow_direction + ) + if free_capacity_in_teu >= required_capacity_in_teu: vehicles_with_sufficient_capacity.append(vehicle) assert free_capacity_in_teu >= 0, f"Vehicle {vehicle} is overloaded, checking for " \ @@ -60,11 +77,11 @@ def get_departing_vehicles( def block_capacity_for_outbound_journey( self, vehicle: Type[AbstractLargeScheduledVehicle], - container: Container + container: Container, ) -> bool: """Updates the cache for faster execution """ - return self.large_scheduled_vehicle_repository.block_capacity_for_outbound_journey( + return self.vehicle_capacity_manager.block_capacity_for_outbound_journey( vehicle=vehicle, container=container ) diff --git a/conflowgen/domain_models/vehicle.py b/conflowgen/domain_models/vehicle.py index b0b830ae..c1a42f56 100644 --- a/conflowgen/domain_models/vehicle.py +++ b/conflowgen/domain_models/vehicle.py @@ -63,7 +63,7 @@ class LargeScheduledVehicle(BaseModel): help_text="This is the vehicle capacity. It can be used, e.g., to determine how many cranes can serve it in " "the subsequent model that reads in this data." ) - moved_capacity = IntegerField( + inbound_container_volume = IntegerField( null=False, help_text="This is the actually moved container volume in TEU for a single terminal visit on the inbound " "journey." diff --git a/conflowgen/flow_generator/allocate_space_for_containers_delivered_by_truck_service.py b/conflowgen/flow_generator/allocate_space_for_containers_delivered_by_truck_service.py index c506dba8..231804ae 100644 --- a/conflowgen/flow_generator/allocate_space_for_containers_delivered_by_truck_service.py +++ b/conflowgen/flow_generator/allocate_space_for_containers_delivered_by_truck_service.py @@ -3,6 +3,8 @@ from typing import Dict, Type, List from conflowgen.application.repositories.random_seed_store_repository import get_initialised_random_object +from conflowgen.application.services.vehicle_capacity_manager import VehicleCapacityManager +from conflowgen.descriptive_datatypes import FlowDirection from conflowgen.domain_models.container import Container from conflowgen.domain_models.distribution_repositories.mode_of_transport_distribution_repository import \ ModeOfTransportDistributionRepository @@ -24,11 +26,12 @@ def __init__(self): self.mode_of_transport_distribution_repository = ModeOfTransportDistributionRepository() self.mode_of_transport_distribution: Dict[ModeOfTransport, Dict[ModeOfTransport, float]] | None = None self.large_scheduled_vehicle_repository = LargeScheduledVehicleRepository() + self.vehicle_capacity_manager = VehicleCapacityManager() self.container_factory = ContainerFactory() def reload_distribution(self, transportation_buffer: float): self.mode_of_transport_distribution = self.mode_of_transport_distribution_repository.get_distribution() - self.large_scheduled_vehicle_repository.set_transportation_buffer( + self.vehicle_capacity_manager.set_transportation_buffer( transportation_buffer=transportation_buffer ) self.logger.info(f"Use transport buffer of {transportation_buffer} for allocating containers delivered by " @@ -62,7 +65,7 @@ def allocate(self) -> None: if truck_to_other_vehicle_distribution[ModeOfTransport.truck] > 0: raise NotImplementedError("Truck to truck traffic is not supported.") - self.large_scheduled_vehicle_repository.reset_cache() + self.vehicle_capacity_manager.reset_cache() number_containers_to_allocate = self._get_number_containers_to_allocate() @@ -114,8 +117,8 @@ def allocate(self) -> None: del truck_to_other_vehicle_distribution[selected_mode_of_transport] # drop this type continue # try again with another vehicle type (refers to while loop) - free_capacity_of_vehicle = self.large_scheduled_vehicle_repository.\ - get_free_capacity_for_outbound_journey(vehicle) + free_capacity_of_vehicle = self.vehicle_capacity_manager.\ + get_free_capacity_for_outbound_journey(vehicle, flow_direction=FlowDirection.export_flow) if free_capacity_of_vehicle <= self.ignored_capacity: @@ -138,7 +141,7 @@ def allocate(self) -> None: container = self.container_factory.create_container_for_delivering_truck(vehicle) teu_total += ContainerLength.get_teu_factor(container.length) - self.large_scheduled_vehicle_repository.block_capacity_for_outbound_journey(vehicle, container) + self.vehicle_capacity_manager.block_capacity_for_outbound_journey(vehicle, container) successful_assignment += 1 break # success, no further looping to search for a suitable vehicle @@ -175,7 +178,8 @@ def _pick_vehicle( # Make it more likely that a container ends up on a large vessel than on a smaller one vehicle: Type[AbstractLargeScheduledVehicle] vehicle_distribution: Dict[Type[AbstractLargeScheduledVehicle], float] = { - vehicle: self.large_scheduled_vehicle_repository.get_free_capacity_for_outbound_journey(vehicle) + vehicle: self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + vehicle, FlowDirection.export_flow) for vehicle in vehicles_of_type } all_free_capacities = list(vehicle_distribution.values()) diff --git a/conflowgen/flow_generator/container_flow_generation_service.py b/conflowgen/flow_generator/container_flow_generation_service.py index 9a772284..7f695bf0 100644 --- a/conflowgen/flow_generator/container_flow_generation_service.py +++ b/conflowgen/flow_generator/container_flow_generation_service.py @@ -43,21 +43,38 @@ 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 = datetime.datetime.combine( + self.container_flow_start_date, datetime.time(hour=0, minute=0, second=0) + ) + datetime.timedelta(days=ramp_up_period) + self.ramp_down_period_start = datetime.datetime.combine( + self.container_flow_end_date, datetime.time(hour=0, minute=0, second=0) + ) - 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 self.large_scheduled_vehicle_for_onward_transportation_manager.reload_properties( - transportation_buffer=self.transportation_buffer + transportation_buffer=self.transportation_buffer, + ramp_up_period_end=self.ramp_up_period_end, + ramp_down_period_start=self.ramp_down_period_start, ) self.allocate_space_for_containers_delivered_by_truck_service.reload_distribution( transportation_buffer=self.transportation_buffer ) + self.truck_for_import_containers_manager.reload_distributions() self.truck_for_export_containers_manager.reload_distributions() + self.large_scheduled_vehicle_creation_service.reload_properties( container_flow_start_date=self.container_flow_start_date, - container_flow_end_date=self.container_flow_end_date + container_flow_end_date=self.container_flow_end_date, + ramp_up_period_end=self.ramp_up_period_end, + ramp_down_period_start=self.ramp_down_period_start, ) + self.assign_destination_to_container_service.reload_distributions() @staticmethod diff --git a/conflowgen/flow_generator/large_scheduled_vehicle_creation_service.py b/conflowgen/flow_generator/large_scheduled_vehicle_creation_service.py index 170d6a0b..62545780 100644 --- a/conflowgen/flow_generator/large_scheduled_vehicle_creation_service.py +++ b/conflowgen/flow_generator/large_scheduled_vehicle_creation_service.py @@ -1,4 +1,7 @@ +from __future__ import annotations + import datetime +import typing from typing import List, Type import logging @@ -29,11 +32,17 @@ def __init__(self): def reload_properties( self, container_flow_start_date: datetime.date, - container_flow_end_date: datetime.date + container_flow_end_date: datetime.date, + ramp_up_period_end: typing.Optional[datetime.datetime | datetime.date], + ramp_down_period_start: typing.Optional[datetime.datetime | datetime.date], ): assert container_flow_start_date < container_flow_end_date self.container_flow_start_date = container_flow_start_date self.container_flow_end_date = container_flow_end_date + self.container_factory.set_ramp_up_and_down_times( + ramp_up_period_end=ramp_up_period_end, + ramp_down_period_start=ramp_down_period_start + ) self.container_factory.reload_distributions() def create(self) -> None: 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..2b281f0a 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,7 +2,7 @@ import datetime import logging import math -from typing import Tuple, List, Dict, Type, Sequence +from typing import Tuple, List, Dict, Type, Sequence, Optional import numpy as np # noinspection PyProtectedMember @@ -31,7 +31,7 @@ def __init__(self): self.logger = logging.getLogger("conflowgen") self.schedule_repository = ScheduleRepository() - self.large_scheduled_vehicle_repository = self.schedule_repository.large_scheduled_vehicle_repository + self.vehicle_capacity_manager = self.schedule_repository.vehicle_capacity_manager self.mode_of_transport_distribution_repository = ModeOfTransportDistributionRepository() self.mode_of_transport_distribution = self.mode_of_transport_distribution_repository.get_distribution() @@ -42,15 +42,23 @@ def __init__(self): def reload_properties( self, - transportation_buffer: float + transportation_buffer: float, + ramp_up_period_end: Optional[datetime.datetime | datetime.date] = None, + ramp_down_period_start: Optional[datetime.datetime | datetime.date] = None, ): assert -1 < transportation_buffer self.schedule_repository.set_transportation_buffer(transportation_buffer) + + self.schedule_repository.set_ramp_up_and_down_times( + ramp_up_period_end=ramp_up_period_end, + ramp_down_period_start=ramp_down_period_start + ) + self.logger.debug(f"Using transportation buffer of {transportation_buffer} when choosing the departing " f"vehicles that adhere a schedule.") self.container_dwell_time_distributions = self.container_dwell_time_distribution_repository.get_distributions() - self.large_scheduled_vehicle_repository = self.schedule_repository.large_scheduled_vehicle_repository + self.vehicle_capacity_manager = self.schedule_repository.vehicle_capacity_manager self.mode_of_transport_distribution = self.mode_of_transport_distribution_repository.get_distribution() def choose_departing_vehicle_for_containers(self) -> None: @@ -66,7 +74,7 @@ def choose_departing_vehicle_for_containers(self) -> None: number_assigned_containers = 0 number_not_assignable_containers = 0 - self.large_scheduled_vehicle_repository.reset_cache() + self.vehicle_capacity_manager.reset_cache() self.logger.info("Assign containers to departing vehicles that move according to a schedule...") @@ -125,7 +133,8 @@ def choose_departing_vehicle_for_containers(self) -> None: start=(container_arrival + datetime.timedelta(hours=minimum_dwell_time_in_hours)), end=(container_arrival + datetime.timedelta(hours=maximum_dwell_time_in_hours)), vehicle_type=initial_departing_vehicle_type, - required_capacity=container.length + required_capacity=container.length, + flow_direction=container.flow_direction ) if len(available_vehicles) > 0: @@ -193,8 +202,12 @@ def _draw_vehicle( return available_vehicles[0] vehicles_and_their_respective_free_capacity = {} + for vehicle in available_vehicles: - free_capacity = self.large_scheduled_vehicle_repository.get_free_capacity_for_outbound_journey(vehicle) + + free_capacity = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + vehicle, container.flow_direction + ) if free_capacity >= ContainerLength.get_teu_factor(ContainerLength.other): vehicles_and_their_respective_free_capacity[vehicle] = free_capacity @@ -313,7 +326,8 @@ def _find_alternative_mode_of_transportation( start=(container_arrival + datetime.timedelta(hours=minimum_dwell_time_in_hours)), end=(container_arrival + datetime.timedelta(hours=maximum_dwell_time_in_hours)), vehicle_type=vehicle_type, - required_capacity=container.length + required_capacity=container.length, + flow_direction=container.flow_direction ) if len(available_vehicles) > 0: # There is a vehicle of a new type available, so it is picked vehicle = self._pick_vehicle_for_container(available_vehicles, container) 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_container_dwell_time_analysis.py b/conflowgen/tests/analyses/test_container_dwell_time_analysis.py index 55bd85c6..eeee565c 100644 --- a/conflowgen/tests/analyses/test_container_dwell_time_analysis.py +++ b/conflowgen/tests/analyses/test_container_dwell_time_analysis.py @@ -53,12 +53,12 @@ def test_with_single_container(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -98,12 +98,12 @@ def test_with_two_containers(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -166,12 +166,12 @@ def test_with_two_containers_and_end_time(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -235,12 +235,12 @@ def test_with_two_containers_and_start_and_end_time(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_container_dwell_time_analysis_report.py b/conflowgen/tests/analyses/test_container_dwell_time_analysis_report.py index b0f27b3d..29bcc6e8 100644 --- a/conflowgen/tests/analyses/test_container_dwell_time_analysis_report.py +++ b/conflowgen/tests/analyses/test_container_dwell_time_analysis_report.py @@ -27,12 +27,12 @@ def setup_feeder_data(): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_container_flow_by_vehicle_type_analysis.py b/conflowgen/tests/analyses/test_container_flow_by_vehicle_type_analysis.py index 65714274..f19dd4b0 100644 --- a/conflowgen/tests/analyses/test_container_flow_by_vehicle_type_analysis.py +++ b/conflowgen/tests/analyses/test_container_flow_by_vehicle_type_analysis.py @@ -61,13 +61,13 @@ def test_with_single_feeder(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -105,13 +105,13 @@ def test_with_single_feeder_with_time_window(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=one_week_later, schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_container_flow_by_vehicle_type_analysis_report.py b/conflowgen/tests/analyses/test_container_flow_by_vehicle_type_analysis_report.py index dd3a4e79..0acb7794 100644 --- a/conflowgen/tests/analyses/test_container_flow_by_vehicle_type_analysis_report.py +++ b/conflowgen/tests/analyses/test_container_flow_by_vehicle_type_analysis_report.py @@ -23,13 +23,13 @@ def setup_feeder_data(): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_container_flow_vehicle_type_adjustment_per_vehicle_analysis.py b/conflowgen/tests/analyses/test_container_flow_vehicle_type_adjustment_per_vehicle_analysis.py index 36bd2d36..6f304623 100644 --- a/conflowgen/tests/analyses/test_container_flow_vehicle_type_adjustment_per_vehicle_analysis.py +++ b/conflowgen/tests/analyses/test_container_flow_vehicle_type_adjustment_per_vehicle_analysis.py @@ -48,13 +48,13 @@ def test_with_feeder_and_truck_and_no_adjustment(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) inbound_feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -64,7 +64,7 @@ def test_with_feeder_and_truck_and_no_adjustment(self): inbound_feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -72,7 +72,7 @@ def test_with_feeder_and_truck_and_no_adjustment(self): outbound_feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder2", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=vessel_arrival, schedule=schedule ) @@ -91,6 +91,7 @@ def test_with_feeder_and_truck_and_no_adjustment(self): self.assertListEqual(list(no_adjustment.values()), [0]) vehicle_identifier = list(no_adjustment.keys())[0] self.assertEqual(vehicle_identifier, VehicleIdentifier( + id=outbound_feeder_lsv.id, mode_of_transport=ModeOfTransport.feeder, vehicle_arrival_time=vessel_arrival, service_name="TestFeederService", @@ -105,13 +106,13 @@ def test_with_feeder_and_truck_and_one_adjusted_box(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -149,13 +150,13 @@ def test_with_feeder_and_truck_and_some_adjustments(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -203,14 +204,14 @@ def test_with_truck_and_feeder_and_no_adjustment(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_arrival = datetime.datetime.now() feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=feeder_arrival, schedule=schedule ) @@ -243,6 +244,7 @@ def test_with_truck_and_feeder_and_no_adjustment(self): vehicle_identifier = list(no_adjustment.keys())[0] self.assertEqual(vehicle_identifier, VehicleIdentifier( + id=feeder_lsv.id, mode_of_transport=ModeOfTransport.feeder, vehicle_arrival_time=feeder_arrival, service_name="TestFeederService", diff --git a/conflowgen/tests/analyses/test_container_flow_vehicle_type_adjustment_per_vehicle_analysis_report.py b/conflowgen/tests/analyses/test_container_flow_vehicle_type_adjustment_per_vehicle_analysis_report.py index 05e41a3f..3751a510 100644 --- a/conflowgen/tests/analyses/test_container_flow_vehicle_type_adjustment_per_vehicle_analysis_report.py +++ b/conflowgen/tests/analyses/test_container_flow_vehicle_type_adjustment_per_vehicle_analysis_report.py @@ -25,13 +25,13 @@ def setup_feeder_data(): vehicle_arrives_at=when.date(), vehicle_arrives_at_time=when.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) inbound_feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -42,7 +42,7 @@ def setup_feeder_data(): outbound_feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder2", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=vessel_arrival, schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_inbound_and_outbound_vehicle_capacity_analysis.py b/conflowgen/tests/analyses/test_inbound_and_outbound_vehicle_capacity_analysis.py index bffa99a4..80ab527f 100644 --- a/conflowgen/tests/analyses/test_inbound_and_outbound_vehicle_capacity_analysis.py +++ b/conflowgen/tests/analyses/test_inbound_and_outbound_vehicle_capacity_analysis.py @@ -66,13 +66,13 @@ def test_inbound_with_single_feeder(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -110,13 +110,13 @@ def test_outbound_with_single_feeder(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_inbound_and_outbound_vehicle_capacity_analysis_report.py b/conflowgen/tests/analyses/test_inbound_and_outbound_vehicle_capacity_analysis_report.py index 5522c95a..923cd77b 100644 --- a/conflowgen/tests/analyses/test_inbound_and_outbound_vehicle_capacity_analysis_report.py +++ b/conflowgen/tests/analyses/test_inbound_and_outbound_vehicle_capacity_analysis_report.py @@ -23,14 +23,14 @@ def setup_feeder_data(): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) schedule.save() feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_inbound_to_outbound_capacity_utilization_analysis.py b/conflowgen/tests/analyses/test_inbound_to_outbound_capacity_utilization_analysis.py index 4d17a8f6..afd2b1dc 100644 --- a/conflowgen/tests/analyses/test_inbound_to_outbound_capacity_utilization_analysis.py +++ b/conflowgen/tests/analyses/test_inbound_to_outbound_capacity_utilization_analysis.py @@ -2,8 +2,8 @@ import unittest from conflowgen.descriptive_datatypes import VehicleIdentifier -from conflowgen.analyses.inbound_to_outbound_vehicle_capacity_utilization_analysis import \ - InboundToOutboundVehicleCapacityUtilizationAnalysis +from conflowgen.analyses.outbound_to_inbound_vehicle_capacity_utilization_analysis import \ + OutboundToInboundVehicleCapacityUtilizationAnalysis from conflowgen.domain_models.container import Container from conflowgen.domain_models.data_types.container_length import ContainerLength from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport @@ -29,7 +29,7 @@ def setUp(self) -> None: Destination ]) mode_of_transport_distribution_seeder.seed() - self.analysis = InboundToOutboundVehicleCapacityUtilizationAnalysis( + self.analysis = OutboundToInboundVehicleCapacityUtilizationAnalysis( transportation_buffer=0.2 ) @@ -53,14 +53,14 @@ def test_inbound_with_single_feeder(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=250, + average_inbound_container_volume=250, vehicle_arrives_every_k_days=-1 ) now = datetime.datetime.now() feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -82,7 +82,7 @@ def test_inbound_with_single_feeder(self): self.assertEqual(len(capacities_with_one_feeder), 1, "There is only one vehicle") key_of_entry: VehicleIdentifier = list(capacities_with_one_feeder.keys())[0] - self.assertEqual(len(key_of_entry), 4, "Key consists of four components") + self.assertEqual(len(key_of_entry), 5, "Key consists of five components") self.assertEqual(key_of_entry.mode_of_transport, ModeOfTransport.feeder) self.assertEqual(key_of_entry.service_name, "TestFeederService") self.assertEqual(key_of_entry.vehicle_name, "TestFeeder1") diff --git a/conflowgen/tests/analyses/test_inbound_to_outbound_capacity_utilization_analysis_report.py b/conflowgen/tests/analyses/test_inbound_to_outbound_capacity_utilization_analysis_report.py index d5513518..887474db 100644 --- a/conflowgen/tests/analyses/test_inbound_to_outbound_capacity_utilization_analysis_report.py +++ b/conflowgen/tests/analyses/test_inbound_to_outbound_capacity_utilization_analysis_report.py @@ -1,7 +1,7 @@ import datetime -from conflowgen.analyses.inbound_to_outbound_vehicle_capacity_utilization_analysis_report import \ - InboundToOutboundVehicleCapacityUtilizationAnalysisReport +from conflowgen.analyses.outbound_to_inbound_vehicle_capacity_utilization_analysis_report import \ + OutboundToInboundVehicleCapacityUtilizationAnalysisReport from conflowgen.application.models.container_flow_generation_properties import ContainerFlowGenerationProperties from conflowgen.domain_models.container import Container from conflowgen.domain_models.data_types.container_length import ContainerLength @@ -23,13 +23,13 @@ def setup_feeder_data(): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=250, + average_inbound_container_volume=250, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -66,7 +66,7 @@ def setUp(self) -> None: start_date=datetime.date(2021, 12, 15), end_date=datetime.date(2021, 12, 17) ) - self.report = InboundToOutboundVehicleCapacityUtilizationAnalysisReport() + self.report = OutboundToInboundVehicleCapacityUtilizationAnalysisReport() def test_with_no_data(self): actual_report = self.report.get_report_as_text() diff --git a/conflowgen/tests/analyses/test_modal_split_analysis.py b/conflowgen/tests/analyses/test_modal_split_analysis.py index 6a5bd5a1..dc18598c 100644 --- a/conflowgen/tests/analyses/test_modal_split_analysis.py +++ b/conflowgen/tests/analyses/test_modal_split_analysis.py @@ -49,13 +49,13 @@ def test_transshipment_share_with_single_feeder(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -84,13 +84,13 @@ def test_outbound_with_single_feeder(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -120,13 +120,13 @@ def test_outbound_with_single_feeder_and_not_affecting_start_and_end(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_modal_split_analysis_report.py b/conflowgen/tests/analyses/test_modal_split_analysis_report.py index 8d090bc4..4f34c408 100644 --- a/conflowgen/tests/analyses/test_modal_split_analysis_report.py +++ b/conflowgen/tests/analyses/test_modal_split_analysis_report.py @@ -22,14 +22,14 @@ def setup_feeder_data(): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) schedule.save() feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_quay_side_throughput_analysis.py b/conflowgen/tests/analyses/test_quay_side_throughput_analysis.py index 448b2a0b..7f452fea 100644 --- a/conflowgen/tests/analyses/test_quay_side_throughput_analysis.py +++ b/conflowgen/tests/analyses/test_quay_side_throughput_analysis.py @@ -48,12 +48,12 @@ def test_with_single_container(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -92,12 +92,12 @@ def test_with_two_containers(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_quay_side_throughput_analysis_report.py b/conflowgen/tests/analyses/test_quay_side_throughput_analysis_report.py index 22c54ea3..a88a6127 100644 --- a/conflowgen/tests/analyses/test_quay_side_throughput_analysis_report.py +++ b/conflowgen/tests/analyses/test_quay_side_throughput_analysis_report.py @@ -25,12 +25,12 @@ def setup_feeder_data(): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -142,12 +142,12 @@ def test_with_train(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=90, - average_moved_capacity=90, + average_inbound_container_volume=90, ) train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=90, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_run_all_analyses.py b/conflowgen/tests/analyses/test_run_all_analyses.py index fee4bdfe..a36f9d07 100644 --- a/conflowgen/tests/analyses/test_run_all_analyses.py +++ b/conflowgen/tests/analyses/test_run_all_analyses.py @@ -1,5 +1,4 @@ import datetime -import unittest import unittest.mock from conflowgen.analyses import run_all_analyses @@ -23,10 +22,11 @@ def setUp(self) -> None: def test_with_no_data(self): with self.assertLogs('conflowgen', level='INFO') as context: run_all_analyses() - self.assertEqual(len(context.output), 35) + self.assertEqual(len(context.output), 38) def test_with_no_data_as_graph(self): - with unittest.mock.patch('matplotlib.pyplot.show'): + 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) + self.assertEqual(len(context.output), 29) diff --git a/conflowgen/tests/analyses/test_truck_gate_throughput_analysis.py b/conflowgen/tests/analyses/test_truck_gate_throughput_analysis.py index 57a380bc..679935e5 100644 --- a/conflowgen/tests/analyses/test_truck_gate_throughput_analysis.py +++ b/conflowgen/tests/analyses/test_truck_gate_throughput_analysis.py @@ -46,12 +46,12 @@ def test_with_single_container(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -90,12 +90,12 @@ def test_with_two_containers(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -153,12 +153,12 @@ def test_with_two_containers_and_end_time(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_truck_gate_throughput_analysis_report.py b/conflowgen/tests/analyses/test_truck_gate_throughput_analysis_report.py index daaf4f01..e75c19eb 100644 --- a/conflowgen/tests/analyses/test_truck_gate_throughput_analysis_report.py +++ b/conflowgen/tests/analyses/test_truck_gate_throughput_analysis_report.py @@ -26,12 +26,12 @@ def setup_feeder_data(): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -136,12 +136,12 @@ def test_with_train(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=90, - average_moved_capacity=90, + average_inbound_container_volume=90, ) train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=90, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_yard_capacity_analysis.py b/conflowgen/tests/analyses/test_yard_capacity_analysis.py index 08f55dc6..3c7ff8f8 100644 --- a/conflowgen/tests/analyses/test_yard_capacity_analysis.py +++ b/conflowgen/tests/analyses/test_yard_capacity_analysis.py @@ -48,12 +48,12 @@ def test_with_single_container(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -96,12 +96,12 @@ def test_with_two_containers(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -163,12 +163,12 @@ def test_with_container_group(self): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv_1 = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) @@ -178,7 +178,7 @@ def test_with_container_group(self): feeder_lsv_2 = LargeScheduledVehicle.create( vehicle_name="TestFeeder2", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now + datetime.timedelta(hours=72), schedule=schedule ) diff --git a/conflowgen/tests/analyses/test_yard_capacity_analysis_report.py b/conflowgen/tests/analyses/test_yard_capacity_analysis_report.py index 9b4c6783..76bb2f48 100644 --- a/conflowgen/tests/analyses/test_yard_capacity_analysis_report.py +++ b/conflowgen/tests/analyses/test_yard_capacity_analysis_report.py @@ -24,12 +24,12 @@ def setup_feeder_data(): vehicle_arrives_at=now.date(), vehicle_arrives_at_time=now.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=300, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=now, schedule=schedule ) diff --git a/conflowgen/tests/api/test_container_flow_generation_manager.py b/conflowgen/tests/api/test_container_flow_generation_manager.py index 62c03e3c..38260ef0 100644 --- a/conflowgen/tests/api/test_container_flow_generation_manager.py +++ b/conflowgen/tests/api/test_container_flow_generation_manager.py @@ -66,18 +66,24 @@ 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 maximum_dwell_time_of_import_containers_in_hours = 40 maximum_dwell_time_of_export_containers_in_hours = 50 maximum_dwell_time_of_transshipment_containers_in_hours = 60 + conflowgen_version = '2.1.1' dict_properties = { '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, + 'conflowgen_version': '2.1.1', } with unittest.mock.patch.object( diff --git a/conflowgen/tests/api/test_port_call_manager.py b/conflowgen/tests/api/test_port_call_manager.py index 04381d35..a6265761 100644 --- a/conflowgen/tests/api/test_port_call_manager.py +++ b/conflowgen/tests/api/test_port_call_manager.py @@ -11,7 +11,7 @@ class TestPortCallManager(unittest.TestCase): def setUp(self) -> None: self.port_call_manager = PortCallManager() - def test_add_vehicle(self): + def test_add_service_that_calls_terminal(self): feeder_service_name = "LX050" arrives_at = datetime.date(2021, 7, 9) time_of_the_day = datetime.time(hour=11) @@ -29,13 +29,13 @@ def test_add_vehicle(self): self.port_call_manager, 'has_schedule', return_value=False) as mock_has_schedule: - self.port_call_manager.add_vehicle( + self.port_call_manager.add_service_that_calls_terminal( vehicle_type=ModeOfTransport.feeder, service_name=feeder_service_name, vehicle_arrives_at=arrives_at, vehicle_arrives_at_time=time_of_the_day, average_vehicle_capacity=total_capacity, - average_moved_capacity=moved_capacity, + average_inbound_container_volume=moved_capacity, next_destinations=next_destinations ) mock_has_schedule.assert_called_once_with( @@ -48,9 +48,49 @@ def test_add_vehicle(self): vehicle_arrives_at=arrives_at, vehicle_arrives_at_time=time_of_the_day, average_vehicle_capacity=total_capacity, - average_moved_capacity=moved_capacity, + average_inbound_container_volume=moved_capacity, + next_destinations=next_destinations, + vehicle_arrives_every_k_days=7 + ) + + def test_add_vehicle(self): + arrives_at = datetime.date(2021, 7, 9) + time_of_the_day = datetime.time(hour=11) + total_capacity = 100 + moved_capacity = 3 + next_destinations = [ + ("DEBRV", 0.6), # 60% of the containers (in boxes) go here... + ("RULED", 0.4) # and the other 40% of the containers (in boxes) go here. + ] + with unittest.mock.patch.object( + self.port_call_manager.schedule_factory, + 'add_schedule', + return_value=None) as mock_add_schedule: + with unittest.mock.patch.object( + self.port_call_manager, + 'has_schedule', + return_value=False) as mock_has_schedule: + self.port_call_manager.add_vehicle( + vehicle_type=ModeOfTransport.feeder, + vehicle_arrives_at=arrives_at, + vehicle_arrives_at_time=time_of_the_day, + vehicle_capacity=total_capacity, + inbound_container_volume=moved_capacity, + next_destinations=next_destinations + ) + mock_has_schedule.assert_called_once_with( + vehicle_type=ModeOfTransport.feeder, + service_name=unittest.mock.ANY, + ) + mock_add_schedule.assert_called_once_with( + vehicle_type=ModeOfTransport.feeder, + service_name=unittest.mock.ANY, + vehicle_arrives_at=arrives_at, + vehicle_arrives_at_time=time_of_the_day, + average_vehicle_capacity=total_capacity, + average_inbound_container_volume=moved_capacity, next_destinations=next_destinations, - vehicle_arrives_every_k_days=None + vehicle_arrives_every_k_days=-1 ) def test_get(self): diff --git a/conflowgen/tests/application/reports/test_container_flow_statistics_report.py b/conflowgen/tests/application/reports/test_container_flow_statistics_report.py index 7e53b888..46f9321f 100644 --- a/conflowgen/tests/application/reports/test_container_flow_statistics_report.py +++ b/conflowgen/tests/application/reports/test_container_flow_statistics_report.py @@ -46,12 +46,12 @@ def _create_feeder(scheduled_arrival: datetime.datetime, service_name_suffix: st vehicle_arrives_at=scheduled_arrival.date(), vehicle_arrives_at_time=scheduled_arrival.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=scheduled_arrival, schedule=schedule ) @@ -135,7 +135,7 @@ def test_empty_ship_using_capacity_as_maximum(self): def test_empty_ship_using_buffer_as_maximum(self): now = datetime.datetime.now() feeder = self._create_feeder(scheduled_arrival=now) - feeder.large_scheduled_vehicle.moved_capacity = 20 + feeder.large_scheduled_vehicle.inbound_container_volume = 20 feeder.large_scheduled_vehicle.save() truck = self._create_truck(arrival=now) self._create_container_delivered_by_truck(truck) @@ -160,7 +160,7 @@ def test_empty_ship_using_buffer_as_maximum(self): def test_inbound_loaded_ship_using_capacity_as_maximum(self): now = datetime.datetime.now() feeder = self._create_feeder(scheduled_arrival=now) - feeder.large_scheduled_vehicle.moved_capacity = 20 + feeder.large_scheduled_vehicle.inbound_container_volume = 20 feeder.large_scheduled_vehicle.save() self._create_container_delivered_by_large_scheduled_vehicle(feeder) self.report.generate() @@ -184,7 +184,7 @@ def test_inbound_loaded_ship_using_capacity_as_maximum(self): def test_outbound_loaded_ship_using_buffer_as_maximum(self): now = datetime.datetime.now() feeder = self._create_feeder(scheduled_arrival=now) - feeder.large_scheduled_vehicle.moved_capacity = 20 + feeder.large_scheduled_vehicle.inbound_container_volume = 20 feeder.large_scheduled_vehicle.save() truck = self._create_truck(arrival=now) container = self._create_container_delivered_by_truck(truck) @@ -214,7 +214,7 @@ def test_outbound_loaded_ship_using_capacity_as_maximum(self): now = datetime.datetime.now() feeder = self._create_feeder(scheduled_arrival=now) feeder.large_scheduled_vehicle.capacity_in_teu = 20 - feeder.large_scheduled_vehicle.moved_capacity = 20 + feeder.large_scheduled_vehicle.inbound_container_volume = 20 feeder.large_scheduled_vehicle.save() truck = self._create_truck(arrival=now) container = self._create_container_delivered_by_truck(truck) @@ -244,11 +244,11 @@ def test_two_ships_one_with_inbound_traffic(self): now = datetime.datetime.now() feeder_1 = self._create_feeder(scheduled_arrival=now, service_name_suffix="1") feeder_1.large_scheduled_vehicle.capacity_in_teu = 20 - feeder_1.large_scheduled_vehicle.moved_capacity = 20 + feeder_1.large_scheduled_vehicle.inbound_container_volume = 20 feeder_1.large_scheduled_vehicle.save() feeder_2 = self._create_feeder(scheduled_arrival=now, service_name_suffix="2") feeder_2.large_scheduled_vehicle.capacity_in_teu = 20 - feeder_2.large_scheduled_vehicle.moved_capacity = 20 + feeder_2.large_scheduled_vehicle.inbound_container_volume = 20 feeder_2.large_scheduled_vehicle.save() self._create_container_delivered_by_large_scheduled_vehicle(feeder_1) @@ -276,11 +276,11 @@ def test_two_loaded_ships_one_with_outbound_traffic(self): now = datetime.datetime.now() feeder_1 = self._create_feeder(scheduled_arrival=now, service_name_suffix="1") feeder_1.large_scheduled_vehicle.capacity_in_teu = 20 - feeder_1.large_scheduled_vehicle.moved_capacity = 20 + feeder_1.large_scheduled_vehicle.inbound_container_volume = 20 feeder_1.large_scheduled_vehicle.save() feeder_2 = self._create_feeder(scheduled_arrival=now, service_name_suffix="2") feeder_2.large_scheduled_vehicle.capacity_in_teu = 20 - feeder_2.large_scheduled_vehicle.moved_capacity = 20 + feeder_2.large_scheduled_vehicle.inbound_container_volume = 20 feeder_2.large_scheduled_vehicle.save() truck = self._create_truck(arrival=now) diff --git a/conflowgen/tests/application/services/test_vehicle_capacity_manager.py b/conflowgen/tests/application/services/test_vehicle_capacity_manager.py new file mode 100644 index 00000000..cc12a47d --- /dev/null +++ b/conflowgen/tests/application/services/test_vehicle_capacity_manager.py @@ -0,0 +1,274 @@ +import datetime +import unittest + +import parameterized + +from conflowgen.application.services.vehicle_capacity_manager import VehicleCapacityManager +from conflowgen.descriptive_datatypes import FlowDirection +from conflowgen.domain_models.container import Container +from conflowgen.domain_models.data_types.container_length import ContainerLength +from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport +from conflowgen.domain_models.data_types.storage_requirement import StorageRequirement +from conflowgen.domain_models.large_vehicle_schedule import Schedule, Destination +from conflowgen.domain_models.vehicle import Train, LargeScheduledVehicle, Truck, Feeder +from conflowgen.tests.substitute_peewee_database import setup_sqlite_in_memory_db + + +class TestVehicleCapacityManager(unittest.TestCase): + + def setUp(self) -> None: + """Create container database in memory""" + sqlite_db = setup_sqlite_in_memory_db() + sqlite_db.create_tables([ + Container, + Schedule, + LargeScheduledVehicle, + Train, + Destination, + Truck, + Feeder, + ]) + + self.vehicle_capacity_manager = VehicleCapacityManager() + self.vehicle_capacity_manager.set_transportation_buffer(transportation_buffer=0) + + schedule_train = Schedule.create( + vehicle_type=ModeOfTransport.train, + service_name="TestServiceTrain", + vehicle_arrives_at=datetime.date(year=2024, month=5, day=26), + vehicle_arrives_at_time=datetime.time(hour=13, minute=15), + average_vehicle_capacity=90, + average_inbound_container_volume=90, + ) + self.train_lsv = LargeScheduledVehicle.create( + vehicle_name="TestTrain1", + capacity_in_teu=90, + inbound_container_volume=30, + scheduled_arrival=datetime.datetime(year=2024, month=5, day=26, hour=13, minute=15), + schedule=schedule_train + ) + self.train = Train.create( + large_scheduled_vehicle=self.train_lsv + ) + + schedule_feeder = Schedule.create( + vehicle_type=ModeOfTransport.feeder, + service_name="TestServiceFeeder", + vehicle_arrives_at=datetime.date(year=2024, month=5, day=26), + vehicle_arrives_at_time=datetime.time(hour=11, minute=15), + average_vehicle_capacity=1200, + average_inbound_container_volume=600, + ) + self.feeder_lsv = LargeScheduledVehicle.create( + vehicle_name="TestFeeder1", + capacity_in_teu=1200, + inbound_container_volume=600, + scheduled_arrival=datetime.datetime(year=2024, month=5, day=26, hour=11, minute=45), + schedule=schedule_feeder + ) + self.feeder = Feeder.create( + large_scheduled_vehicle=self.feeder_lsv + ) + + @parameterized.parameterized.expand([ + [FlowDirection.export_flow], + [FlowDirection.transshipment_flow] + ]) + def test_free_capacity_on_outbound_journey_without_any_containers_and_no_ramp_up_period( + self, flow_direction: FlowDirection + ): + """ + Independent of the flow direction, the outbound capacity should not change as long as no ramp-up period is + defined. + """ + free_capacity_on_train = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + self.train, flow_direction + ) + self.assertEqual(free_capacity_on_train, 30) + + free_capacity_on_feeder = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + self.feeder, flow_direction + ) + self.assertEqual(free_capacity_on_feeder, 600) + + @parameterized.parameterized.expand([ + [FlowDirection.import_flow, 30, 600], + [FlowDirection.export_flow, 30, 600], + [FlowDirection.transshipment_flow, 6, 120], # downscale by 20% + ]) + def test_free_capacity_on_outbound_journey_without_any_containers_and_a_ramp_up_period( + self, flow_direction: FlowDirection, train_volume: int, feeder_volume: int + ): + """ + The outbound capacity should change for transshipment when a ramp-up period is defined and is applicable. + """ + self.vehicle_capacity_manager.set_ramp_up_and_down_times( + ramp_up_period_end=datetime.datetime(year=2024, month=5, day=27) # one day after the feeder and train + ) + train_volume_calc = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + self.train, flow_direction + ) + self.assertEqual( + train_volume, train_volume_calc, f"The used TEU capacity of the train is {train_volume} TEU." + ) + + feeder_volume_calc = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + self.feeder, flow_direction + ) + self.assertEqual( + feeder_volume, feeder_volume_calc, f"The used TEU capacity of the feeder is {feeder_volume} TEU." + ) + + def test_free_capacity_on_inbound_journey_without_any_containers_and_no_ramp_down_period(self): + """ + Independent of the flow direction, the inbound capacity should not change as long as no ramp-down period is + defined. + """ + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( + self.train + ) + self.assertEqual(free_capacity_in_teu, 30, "The used TEU capacity of the train is 30 TEU.") + + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( + self.feeder + ) + self.assertEqual(free_capacity_in_teu, 600, "The used TEU capacity of the feeder is 600 TEU.") + + def test_free_capacity_on_inbound_journey_without_any_containers_and_a_ramp_down_period(self): + """ + Independent of the flow direction, the inbound capacity is capped during the ramp-down period. + """ + self.vehicle_capacity_manager.set_ramp_up_and_down_times( + ramp_down_period_start=datetime.datetime(year=2024, month=5, day=25) # one day before the feeder and train + ) + + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( + self.train + ) + self.assertEqual( + free_capacity_in_teu, 6, "The used TEU capacity of the train is 20% of 30 TEU." + ) + + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( + self.feeder + ) + self.assertEqual( + free_capacity_in_teu, 120, "The used TEU capacity of the feeder is 20% of 600 TEU." + ) + + def test_free_capacity_for_one_teu(self): + """No ramp-up or ramp-down period applied""" + Container.create( + weight=20, + length=ContainerLength.twenty_feet, + storage_requirement=StorageRequirement.standard, + delivered_by=ModeOfTransport.truck, + picked_up_by=ModeOfTransport.train, + picked_up_by_initial=ModeOfTransport.feeder, + picked_up_by_large_scheduled_vehicle=self.train_lsv, + ) + + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + self.train, FlowDirection.undefined + ) + self.assertEqual( + free_capacity_in_teu, 29, "The used TEU capacity of the train is 30 TEU, and 1 TEU is used by " + "the one container we just created.") + + def test_free_capacity_during_ramp_up_period_for_one_teu(self): + Container.create( + weight=20, + length=ContainerLength.forty_feet, + storage_requirement=StorageRequirement.standard, + delivered_by=ModeOfTransport.truck, + picked_up_by=ModeOfTransport.feeder, + picked_up_by_initial=ModeOfTransport.feeder, + picked_up_by_large_scheduled_vehicle=self.feeder_lsv, + ) + self.vehicle_capacity_manager.set_ramp_up_and_down_times( + # one day after the feeder + ramp_up_period_end=datetime.datetime(year=2024, month=5, day=27) + ) + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + self.feeder, FlowDirection.transshipment_flow + ) + self.assertAlmostEqual(free_capacity_in_teu, 118, msg="20% of 600 TEU is 120, of that 2 TEU minus") + + def test_free_capacity_during_ramp_down_period_for_one_teu(self): + Container.create( + weight=20, + length=ContainerLength.twenty_feet, + storage_requirement=StorageRequirement.standard, + delivered_by=ModeOfTransport.train, + picked_up_by=ModeOfTransport.feeder, + picked_up_by_initial=ModeOfTransport.feeder, + delivered_by_large_scheduled_vehicle=self.train_lsv, + ) + self.vehicle_capacity_manager.set_ramp_up_and_down_times( + # one day before the train + ramp_down_period_start=datetime.datetime(year=2024, month=5, day=25) + ) + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( + self.train + ) + self.assertAlmostEqual( + free_capacity_in_teu, 5.0, msg="20% of 30 TEU is 6 TEU, of that 1 TEU is used " + ) + + def test_free_capacity_during_ramp_up_period_without_load(self): + self.vehicle_capacity_manager.set_ramp_up_and_down_times( + # one day before the train + ramp_down_period_start=datetime.datetime(year=2024, month=5, day=25) + ) + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_inbound_journey( + self.train + ) + self.assertAlmostEqual(free_capacity_in_teu, 6) + + def test_free_capacity_for_one_ffe(self): + Container.create( + weight=20, + length=ContainerLength.forty_feet, + storage_requirement=StorageRequirement.standard, + delivered_by=ModeOfTransport.truck, + picked_up_by=ModeOfTransport.train, + picked_up_by_initial=ModeOfTransport.train, + picked_up_by_large_scheduled_vehicle=self.train_lsv, + ) + + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + self.train, FlowDirection.undefined + ) + self.assertEqual(free_capacity_in_teu, 28) # 30 - 2.5 + + def test_free_capacity_for_45_foot_container(self): + Container.create( + weight=20, + length=ContainerLength.forty_five_feet, + storage_requirement=StorageRequirement.standard, + delivered_by=ModeOfTransport.truck, + picked_up_by=ModeOfTransport.train, + picked_up_by_initial=ModeOfTransport.feeder, + picked_up_by_large_scheduled_vehicle=self.train_lsv, + ) + + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + self.train, FlowDirection.undefined + ) + self.assertEqual(free_capacity_in_teu, 27.75, "A 45' container uses 2.25 TEU") + + def test_free_capacity_for_other_container(self): + Container.create( + weight=20, + length=ContainerLength.other, + storage_requirement=StorageRequirement.standard, + delivered_by=ModeOfTransport.truck, + picked_up_by=ModeOfTransport.train, + picked_up_by_initial=ModeOfTransport.feeder, + picked_up_by_large_scheduled_vehicle=self.train_lsv, + ) + + free_capacity_in_teu = self.vehicle_capacity_manager.get_free_capacity_for_outbound_journey( + self.train, FlowDirection.undefined + ) + self.assertEqual(free_capacity_in_teu, 27.5, "30 TEU minus 2.5 TEU") diff --git a/conflowgen/tests/application/services/test_vehicle_countainer_volume_calculator.py b/conflowgen/tests/application/services/test_vehicle_countainer_volume_calculator.py new file mode 100644 index 00000000..d62c8443 --- /dev/null +++ b/conflowgen/tests/application/services/test_vehicle_countainer_volume_calculator.py @@ -0,0 +1,189 @@ +import datetime +import unittest + +import parameterized + +from conflowgen.descriptive_datatypes import FlowDirection +from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport +from conflowgen.application.services.vehicle_container_volume_calculator import VehicleContainerVolumeCalculator +from conflowgen.domain_models.large_vehicle_schedule import Schedule +from conflowgen.domain_models.vehicle import LargeScheduledVehicle, DeepSeaVessel, Feeder +from conflowgen.tests.substitute_peewee_database import setup_sqlite_in_memory_db + + +class TestVehicleContainerVolumeCalculator(unittest.TestCase): + + def setUp(self) -> None: + """Create database in memory""" + self.sqlite_db = setup_sqlite_in_memory_db() + self.sqlite_db.create_tables([ + LargeScheduledVehicle, + Schedule, + DeepSeaVessel, + Feeder, + ]) + self.calculator = VehicleContainerVolumeCalculator() + self.calculator.set_transportation_buffer(0) + + schedule_deep_sea_vessel = Schedule.create( + vehicle_type=ModeOfTransport.deep_sea_vessel, + service_name="TestService-DeepSeaVessel", + vehicle_arrives_at=datetime.date(year=2024, month=8, day=7), + vehicle_arrives_at_time=datetime.time(hour=13, minute=15), + average_vehicle_capacity=12000, + average_inbound_container_volume=3000, + ) + self.lsv_deep_sea_vessel = LargeScheduledVehicle.create( + vehicle_name="TestShip-DeepSeaVessel-1", + capacity_in_teu=12000, + inbound_container_volume=3000, + scheduled_arrival=datetime.datetime(year=2024, month=8, day=7, hour=13, minute=15), + schedule=schedule_deep_sea_vessel + ) + self.deep_sea_vessel = DeepSeaVessel.create( + large_scheduled_vehicle=self.lsv_deep_sea_vessel + ) + + schedule_feeder = Schedule.create( + vehicle_type=ModeOfTransport.feeder, + service_name="TestService-Feeder", + vehicle_arrives_at=datetime.date(year=2024, month=8, day=7), + vehicle_arrives_at_time=datetime.time(hour=13, minute=15), + average_vehicle_capacity=1200, + average_inbound_container_volume=600, + ) + self.lsv_feeder = LargeScheduledVehicle.create( + vehicle_name="TestShip-Feeder1", + capacity_in_teu=1200, + inbound_container_volume=600, + scheduled_arrival=datetime.datetime(year=2024, month=8, day=7, hour=11, minute=9), + schedule=schedule_feeder + ) + self.feeder = Feeder.create( + large_scheduled_vehicle=self.lsv_feeder + ) + + @parameterized.parameterized.expand([ + [flow_direction] for flow_direction in FlowDirection + ]) + def test_get_maximum_transported_container_volume_on_outbound_journey_and_no_ramp_up_period( + self, flow_direction: FlowDirection + ): + """ + Independent of the flow direction, the outbound capacity should not change as long as no ramp-up period is + defined. + """ + feeder_vessel_calc = self.calculator.get_maximum_transported_container_volume_on_outbound_journey( + self.lsv_feeder, flow_direction + ) + scaled_moved_container_volume, unscaled_moved_container_volume = feeder_vessel_calc + self.assertEqual(scaled_moved_container_volume, 600) + + deep_sea_vessel_calc = self.calculator.get_maximum_transported_container_volume_on_outbound_journey( + self.deep_sea_vessel, flow_direction + ) + scaled_moved_container_volume, unscaled_moved_container_volume = deep_sea_vessel_calc + self.assertEqual(scaled_moved_container_volume, 3000) + + @parameterized.parameterized.expand([ + [FlowDirection.import_flow, 600, 3000], # normal values + [FlowDirection.export_flow, 600, 3000], # normal values + [FlowDirection.transshipment_flow, 120, 600], # downscaled by 20% + ]) + def test_get_maximum_transported_container_volume_on_outbound_journey_and_an_applicable_ramp_up_period( + self, flow_direction: FlowDirection, feeder_volume: int, deep_sea_volume: int + ): + """ + The outbound capacity should change for transshipment when a ramp-up period is defined and is applicable. + """ + self.calculator.set_ramp_up_and_down_times( + ramp_up_period_end=datetime.datetime(year=2024, month=8, day=8) # one day after the two vessels + ) + volume_feeder = self.calculator.get_maximum_transported_container_volume_on_outbound_journey( + self.feeder, flow_direction + ) + scaled_moved_container_volume, unscaled_moved_container_volume = volume_feeder + self.assertEqual(scaled_moved_container_volume, feeder_volume) + + volume_deep_sea_vessel = self.calculator.get_maximum_transported_container_volume_on_outbound_journey( + self.deep_sea_vessel, flow_direction + ) + scaled_moved_container_volume, unscaled_moved_container_volume = volume_deep_sea_vessel + self.assertEqual(scaled_moved_container_volume, deep_sea_volume) + + @parameterized.parameterized.expand([ + [FlowDirection.import_flow, 600, 3000], + [FlowDirection.export_flow, 600, 3000], + [FlowDirection.transshipment_flow, 600, 3000], + ]) + def test_get_maximum_transported_container_volume_on_outbound_journey_and_a_non_applicable_ramp_up_period( + self, flow_direction: FlowDirection, feeder_volume: int, deep_sea_volume: int + ): + """ + The outbound capacity should not change for transshipment when a ramp-up period is defined but lies in the past. + """ + self.calculator.set_ramp_up_and_down_times( + ramp_up_period_end=datetime.datetime(year=2024, month=8, day=6) # one day after the two vessels + ) + feeder_volume_calc = self.calculator.get_maximum_transported_container_volume_on_outbound_journey( + self.feeder, flow_direction + ) + scaled_moved_container_volume, unscaled_moved_container_volume = feeder_volume_calc + self.assertEqual(scaled_moved_container_volume, feeder_volume) + + deep_sea_volume_calc = self.calculator.get_maximum_transported_container_volume_on_outbound_journey( + self.deep_sea_vessel, flow_direction + ) + scaled_moved_container_volume, unscaled_moved_container_volume = deep_sea_volume_calc + self.assertEqual(scaled_moved_container_volume, deep_sea_volume) + + def test_get_transported_container_volume_on_inbound_journey_and_no_ramp_down_period(self): + """ + Independent of the flow direction, the inbound capacity should not change as long as no ramp-down period is + defined. + """ + feeder_volume_calc = self.calculator.get_transported_container_volume_on_inbound_journey( + self.feeder + ) + self.assertEqual(feeder_volume_calc, 600) + + deep_sea_vessel_calc = self.calculator.get_transported_container_volume_on_inbound_journey( + self.deep_sea_vessel + ) + self.assertEqual(deep_sea_vessel_calc, 3000) + + def test_get_transported_container_volume_on_inbound_journey_and_an_applicable_ramp_down_period(self): + """ + Independent of the flow direction, the inbound capacity is capped during the ramp-down period. + """ + self.calculator.set_ramp_up_and_down_times( + ramp_down_period_start=datetime.datetime(year=2021, month=8, day=6) # one day before the feeder and train + ) + + feeder_vessel_calc = self.calculator.get_transported_container_volume_on_inbound_journey( + self.lsv_feeder + ) + self.assertEqual(feeder_vessel_calc, 120, "Downscaled by 20%") + + deep_sea_vessel_calc = self.calculator.get_transported_container_volume_on_inbound_journey( + self.lsv_deep_sea_vessel + ) + self.assertEqual(deep_sea_vessel_calc, 600, "Downscaled by 20%") + + def test_get_transported_container_volume_on_inbound_journey_and_a_non_applicable_ramp_down_period(self): + """ + Independent of the flow direction, the inbound capacity is capped during the ramp-down period. + """ + self.calculator.set_ramp_up_and_down_times( + ramp_down_period_start=datetime.datetime(year=2024, month=8, day=8) # one day after + ) + + feeder_volume_calc = self.calculator.get_transported_container_volume_on_inbound_journey( + self.lsv_feeder + ) + self.assertEqual(feeder_volume_calc, 600, "Nothing should be downscaled") + + deep_sea_vessel_volume_calc = self.calculator.get_transported_container_volume_on_inbound_journey( + self.lsv_deep_sea_vessel + ) + self.assertEqual(deep_sea_vessel_volume_calc, 3000, "Nothing should be downscaled") diff --git a/conflowgen/tests/data_summaries/test_data_summaries_cache.py b/conflowgen/tests/data_summaries/test_data_summaries_cache.py index 6448b8d9..df416120 100644 --- a/conflowgen/tests/data_summaries/test_data_summaries_cache.py +++ b/conflowgen/tests/data_summaries/test_data_summaries_cache.py @@ -132,7 +132,7 @@ def test_with_preview(self): vehicle_arrives_every_k_days=-1, vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300 + average_inbound_container_volume=300 ) preview = self.preview.get_weekly_truck_arrivals(True, True) self.assertEqual(preview, {3: 12, 4: 48}, "Uncached result is incorrect") @@ -179,7 +179,7 @@ def test_with_adjusted_preview(self): vehicle_arrives_every_k_days=-1, vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300 + average_inbound_container_volume=300 ) preview = self.preview.get_weekly_truck_arrivals(True, True) self.assertEqual(preview, {3: 12, 4: 48}, "Uncached result is incorrect") diff --git a/conflowgen/tests/domain_models/distribution_repositories/test_container_destination_distribution_repository.py b/conflowgen/tests/domain_models/distribution_repositories/test_container_destination_distribution_repository.py index b527c249..09b706d7 100644 --- a/conflowgen/tests/domain_models/distribution_repositories/test_container_destination_distribution_repository.py +++ b/conflowgen/tests/domain_models/distribution_repositories/test_container_destination_distribution_repository.py @@ -31,7 +31,7 @@ def test_set_distribution(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) Schedule.create( @@ -40,7 +40,7 @@ def test_set_distribution(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) destination_1 = Destination.create( @@ -77,7 +77,7 @@ def test_save_and_load_correspond_for_single_entry(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) schedule.save() @@ -87,7 +87,7 @@ def test_save_and_load_correspond_for_single_entry(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ).save() destination_1 = Destination.create( @@ -121,7 +121,7 @@ def test_save_and_load_correspond_for_two_entries(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) schedule_2 = Schedule.create( @@ -130,7 +130,7 @@ def test_save_and_load_correspond_for_two_entries(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=12000, - average_moved_capacity=1000, + average_inbound_container_volume=1000, vehicle_arrives_every_k_days=-1 ) destination_1 = Destination.create( @@ -180,7 +180,7 @@ def test_validator(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) Schedule.create( @@ -189,7 +189,7 @@ def test_validator(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) destination_1 = Destination.create( diff --git a/conflowgen/tests/domain_models/factories/test_container_factory__create_for_large_scheduled_vehicle.py b/conflowgen/tests/domain_models/factories/test_container_factory__create_for_large_scheduled_vehicle.py index ccd84951..63c13354 100644 --- a/conflowgen/tests/domain_models/factories/test_container_factory__create_for_large_scheduled_vehicle.py +++ b/conflowgen/tests/domain_models/factories/test_container_factory__create_for_large_scheduled_vehicle.py @@ -14,7 +14,7 @@ from conflowgen.domain_models.factories.fleet_factory import FleetFactory from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport from conflowgen.domain_models.large_vehicle_schedule import Destination -from conflowgen.domain_models.vehicle import Feeder, LargeScheduledVehicle, Schedule, Truck +from conflowgen.domain_models.vehicle import Feeder, LargeScheduledVehicle, Schedule, Truck, DeepSeaVessel from conflowgen.tests.substitute_peewee_database import setup_sqlite_in_memory_db @@ -25,6 +25,7 @@ def setUp(self) -> None: sqlite_db = setup_sqlite_in_memory_db() sqlite_db.create_tables([ Feeder, + DeepSeaVessel, LargeScheduledVehicle, Schedule, Container, @@ -47,7 +48,7 @@ def setUp(self) -> None: vehicle_arrives_at=datetime.date(2021, 7, 9), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=3 + average_inbound_container_volume=3 ) self.feeders = FleetFactory().create_feeder_fleet( schedule=schedule, @@ -81,3 +82,69 @@ def test_create_containers_for_feeder_vessel(self) -> None: feeder_1.large_scheduled_vehicle ) self.assertIsNone(containers[0].delivered_by_truck) + + def test_create_containers_for_single_deep_sea_vessel_during_ramp_up_period(self): + schedule = Schedule.create( + service_name="SunExpress", + vehicle_type=ModeOfTransport.deep_sea_vessel, + vehicle_arrives_at=datetime.date(2021, 7, 9), + vehicle_arrives_at_time=datetime.time(11), + average_vehicle_capacity=24000, + average_inbound_container_volume=3000 + ) + vessels = FleetFactory().create_deep_sea_vessel_fleet( + schedule=schedule, + first_at=datetime.date(2021, 7, 8), + latest_at=datetime.date(2021, 7, 10) + ) + self.assertEqual( + len(vessels), + 1 + ) + vessel = vessels[0] + + self.container_factory.set_ramp_up_and_down_times( + ramp_up_period_end=datetime.datetime(2021, 7, 10), + ramp_down_period_start=None + ) + + # noinspection PyTypeChecker + containers = self.container_factory.create_containers_for_large_scheduled_vehicle(vessel) + + container_volume = sum(c.occupied_teu for c in containers) + + self.assertGreater(container_volume, 2900, "A bit less than 3000 is acceptable but common!") + self.assertLess(container_volume, 3100, "A bit more than 3000 is acceptable but common!") + + def test_create_containers_for_single_deep_sea_vessel_during_ramp_down_period(self): + schedule = Schedule.create( + service_name="SunExpress", + vehicle_type=ModeOfTransport.deep_sea_vessel, + vehicle_arrives_at=datetime.date(2024, 7, 9), + vehicle_arrives_at_time=datetime.time(11), + average_vehicle_capacity=24000, + average_inbound_container_volume=3000 + ) + vessels = FleetFactory().create_deep_sea_vessel_fleet( + schedule=schedule, + first_at=datetime.date(2024, 7, 8), + latest_at=datetime.date(2024, 7, 10) + ) + self.assertEqual( + len(vessels), + 1 + ) + vessel = vessels[0] + + self.container_factory.set_ramp_up_and_down_times( + ramp_up_period_end=None, + ramp_down_period_start=datetime.datetime(2024, 7, 8) + ) + + # noinspection PyTypeChecker + containers = self.container_factory.create_containers_for_large_scheduled_vehicle(vessel) + + container_volume_in_teu = sum(c.occupied_teu for c in containers) + + self.assertGreater(container_volume_in_teu, 500, "A bit less than 600 is acceptable but common!") + self.assertLess(container_volume_in_teu, 700, "A bit more than 600 is acceptable but common!") diff --git a/conflowgen/tests/domain_models/factories/test_container_factory__create_for_truck.py b/conflowgen/tests/domain_models/factories/test_container_factory__create_for_truck.py index b873d5e0..d9ed2b85 100644 --- a/conflowgen/tests/domain_models/factories/test_container_factory__create_for_truck.py +++ b/conflowgen/tests/domain_models/factories/test_container_factory__create_for_truck.py @@ -53,7 +53,7 @@ def setUp(self) -> None: vehicle_arrives_at=datetime.date(2021, 7, 9), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=1 + average_inbound_container_volume=1 ) feeders = FleetFactory().create_feeder_fleet( schedule=schedule, diff --git a/conflowgen/tests/domain_models/factories/test_fleet_factory__create_feeder_fleet.py b/conflowgen/tests/domain_models/factories/test_fleet_factory__create_feeder_fleet.py index 03e00bf8..315ff443 100644 --- a/conflowgen/tests/domain_models/factories/test_fleet_factory__create_feeder_fleet.py +++ b/conflowgen/tests/domain_models/factories/test_fleet_factory__create_feeder_fleet.py @@ -26,7 +26,7 @@ def test_create_feeder_fleet(self) -> None: vehicle_arrives_at=datetime.date(2021, 7, 9), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=50 + average_inbound_container_volume=50 ) feeders = self.fleet_factory.create_feeder_fleet( schedule=schedule, diff --git a/conflowgen/tests/domain_models/factories/test_schedule_factory.py b/conflowgen/tests/domain_models/factories/test_schedule_factory.py index a40d20ab..f4c9d77b 100644 --- a/conflowgen/tests/domain_models/factories/test_schedule_factory.py +++ b/conflowgen/tests/domain_models/factories/test_schedule_factory.py @@ -28,7 +28,7 @@ def test_add_schedule(self) -> None: vehicle_arrives_at=datetime.date(2021, 7, 9), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=1, + average_inbound_container_volume=1, next_destinations=[ ("DEBRV", 0.6), ("CNSHG", 0.4) @@ -39,7 +39,7 @@ def test_add_schedule(self) -> None: self.assertEqual(schedule.vehicle_type, ModeOfTransport.feeder) self.assertEqual(schedule.vehicle_arrives_at, datetime.date(2021, 7, 9)) self.assertEqual(schedule.average_vehicle_capacity, 800) - self.assertEqual(schedule.average_moved_capacity, 1) + self.assertEqual(schedule.average_inbound_container_volume, 1) next_destinations = Destination.select().where(Destination.belongs_to_schedule == schedule) next_destinations = list(next_destinations) self.assertEqual(len(next_destinations), 2) @@ -54,7 +54,7 @@ def test_repeated_add_schedule(self) -> None: vehicle_arrives_at=datetime.date(2021, 7, 9), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=1, + average_inbound_container_volume=1, next_destinations=[ ("DEBRV", 0.6), ("CNSHG", 0.4) @@ -67,7 +67,7 @@ def test_repeated_add_schedule(self) -> None: vehicle_arrives_at=datetime.date(2021, 7, 10), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=1, + average_inbound_container_volume=1, next_destinations=[ ("DEBRV", 0.6), ("CNSHG", 0.4) @@ -82,7 +82,7 @@ def test_get_schedule(self) -> None: vehicle_arrives_at=datetime.date(2021, 7, 9), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=1, + average_inbound_container_volume=1, next_destinations=[ ("DEBRV", 0.6), ("CNSHG", 0.4) @@ -93,7 +93,7 @@ def test_get_schedule(self) -> None: self.assertEqual(schedule.vehicle_type, ModeOfTransport.feeder) self.assertEqual(schedule.vehicle_arrives_at, datetime.date(2021, 7, 9)) self.assertEqual(schedule.average_vehicle_capacity, 800) - self.assertEqual(schedule.average_moved_capacity, 1) + self.assertEqual(schedule.average_inbound_container_volume, 1) next_destinations = Destination.select().where(Destination.belongs_to_schedule == schedule) next_destinations = list(next_destinations) self.assertEqual(len(next_destinations), 2) diff --git a/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_barge.py b/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_barge.py index 5daf03f7..cf8c426d 100644 --- a/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_barge.py +++ b/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_barge.py @@ -25,11 +25,11 @@ def test_create_normal_barge(self) -> None: vehicle_type=ModeOfTransport.barge, vehicle_arrives_at=datetime.datetime.now(), average_vehicle_capacity=60, - average_moved_capacity=30 + average_inbound_container_volume=30 ) self.vehicle_factory.create_barge( capacity_in_teu=60, - moved_capacity=30, + inbound_container_volume=30, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -40,19 +40,19 @@ def test_create_unrealistic_barge(self) -> None: vehicle_type=ModeOfTransport.barge, vehicle_arrives_at=datetime.datetime.now(), average_vehicle_capacity=800, - average_moved_capacity=50 + average_inbound_container_volume=50 ) with self.assertRaises(UnrealisticValuesException): self.vehicle_factory.create_barge( capacity_in_teu=-1, - moved_capacity=1, + inbound_container_volume=1, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) with self.assertRaises(UnrealisticValuesException): self.vehicle_factory.create_barge( capacity_in_teu=1, - moved_capacity=-1, + inbound_container_volume=-1, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) diff --git a/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_deep_sea_vessel.py b/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_deep_sea_vessel.py index d68204c9..4704b6ed 100644 --- a/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_deep_sea_vessel.py +++ b/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_deep_sea_vessel.py @@ -25,11 +25,11 @@ def test_create_normal_deep_sea_vessel(self) -> None: vehicle_type=ModeOfTransport.deep_sea_vessel, vehicle_arrives_at=datetime.datetime.now(), average_vehicle_capacity=800, - average_moved_capacity=50 + average_inbound_container_volume=50 ) self.vehicle_factory.create_deep_sea_vessel( capacity_in_teu=800, - moved_capacity=50, + inbound_container_volume=50, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -40,26 +40,26 @@ def test_create_unrealistic_deep_sea_vessel(self) -> None: vehicle_type=ModeOfTransport.deep_sea_vessel, vehicle_arrives_at=datetime.datetime.now(), average_vehicle_capacity=800, - average_moved_capacity=50 + average_inbound_container_volume=50 ) with self.assertRaises(UnrealisticValuesException): self.vehicle_factory.create_deep_sea_vessel( capacity_in_teu=-1, - moved_capacity=1, + inbound_container_volume=1, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) with self.assertRaises(UnrealisticValuesException): self.vehicle_factory.create_deep_sea_vessel( capacity_in_teu=1, - moved_capacity=-1, + inbound_container_volume=-1, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) with self.assertRaises(UnrealisticValuesException): self.vehicle_factory.create_deep_sea_vessel( capacity_in_teu=50, - moved_capacity=100, + inbound_container_volume=100, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) diff --git a/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_feeder.py b/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_feeder.py index d7894cfc..09517939 100644 --- a/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_feeder.py +++ b/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_feeder.py @@ -25,11 +25,11 @@ def test_create_normal_feeder(self) -> None: vehicle_type=ModeOfTransport.feeder, vehicle_arrives_at=datetime.datetime.now(), average_vehicle_capacity=800, - average_moved_capacity=50 + average_inbound_container_volume=50 ) self.vehicle_factory.create_feeder( capacity_in_teu=800, - moved_capacity=50, + inbound_container_volume=50, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -40,12 +40,12 @@ def test_create_unrealistic_feeder(self) -> None: vehicle_type=ModeOfTransport.feeder, vehicle_arrives_at=datetime.datetime.now(), average_vehicle_capacity=800, - average_moved_capacity=50 + average_inbound_container_volume=50 ) with self.assertRaises(UnrealisticValuesException): self.vehicle_factory.create_feeder( capacity_in_teu=-1, - moved_capacity=1, + inbound_container_volume=1, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -53,7 +53,7 @@ def test_create_unrealistic_feeder(self) -> None: with self.assertRaises(UnrealisticValuesException): self.vehicle_factory.create_feeder( capacity_in_teu=1, - moved_capacity=-1, + inbound_container_volume=-1, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) diff --git a/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_train.py b/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_train.py index d613dbc5..2aa44c11 100644 --- a/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_train.py +++ b/conflowgen/tests/domain_models/factories/test_vehicle_factory__create_train.py @@ -25,11 +25,11 @@ def test_create_normal_train(self) -> None: vehicle_type=ModeOfTransport.train, vehicle_arrives_at=datetime.datetime.now(), average_vehicle_capacity=90, - average_moved_capacity=90 + average_inbound_container_volume=90 ) self.vehicle_factory.create_train( capacity_in_teu=90, - moved_capacity=90, + inbound_container_volume=90, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) @@ -40,12 +40,12 @@ def test_create_unrealistic_train(self) -> None: vehicle_type=ModeOfTransport.train, vehicle_arrives_at=datetime.datetime.now(), average_vehicle_capacity=800, - average_moved_capacity=50 + average_inbound_container_volume=50 ) with self.assertRaises(UnrealisticValuesException): self.vehicle_factory.create_train( capacity_in_teu=-1, - moved_capacity=1, + inbound_container_volume=1, scheduled_arrival=datetime.datetime.now(), schedule=schedule ) diff --git a/conflowgen/tests/domain_models/reposistories/test_large_scheduled_vehicle_repository.py b/conflowgen/tests/domain_models/reposistories/test_large_scheduled_vehicle_repository.py deleted file mode 100644 index 423db374..00000000 --- a/conflowgen/tests/domain_models/reposistories/test_large_scheduled_vehicle_repository.py +++ /dev/null @@ -1,104 +0,0 @@ -import datetime -import unittest - -from conflowgen.domain_models.container import Container -from conflowgen.domain_models.data_types.container_length import ContainerLength -from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport -from conflowgen.domain_models.data_types.storage_requirement import StorageRequirement -from conflowgen.domain_models.large_vehicle_schedule import Schedule, Destination -from conflowgen.domain_models.repositories.large_scheduled_vehicle_repository import LargeScheduledVehicleRepository -from conflowgen.domain_models.vehicle import Train, LargeScheduledVehicle, Truck -from conflowgen.tests.substitute_peewee_database import setup_sqlite_in_memory_db - - -class TestLargeScheduledVehicleRepository(unittest.TestCase): - - def setUp(self) -> None: - """Create container database in memory""" - sqlite_db = setup_sqlite_in_memory_db() - sqlite_db.create_tables([ - Container, - Schedule, - LargeScheduledVehicle, - Train, - Destination, - Truck - ]) - self.lsv_repository = LargeScheduledVehicleRepository() - self.lsv_repository.set_transportation_buffer(transportation_buffer=0) - schedule = Schedule.create( - vehicle_type=ModeOfTransport.train, - service_name="TestService", - vehicle_arrives_at=datetime.date(year=2021, month=8, day=7), - vehicle_arrives_at_time=datetime.time(hour=13, minute=15), - average_vehicle_capacity=90, - average_moved_capacity=90, - ) - self.train_lsv = LargeScheduledVehicle.create( - vehicle_name="TestTrain1", - capacity_in_teu=90, - moved_capacity=3, - scheduled_arrival=datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15), - schedule=schedule - ) - self.train_lsv.save() - self.train = Train.create( - large_scheduled_vehicle=self.train_lsv - ) - self.train.save() - - def test_free_capacity_for_one_teu(self): - Container.create( - weight=20, - length=ContainerLength.twenty_feet, - storage_requirement=StorageRequirement.standard, - delivered_by=ModeOfTransport.truck, - picked_up_by=ModeOfTransport.train, - picked_up_by_initial=ModeOfTransport.feeder, - picked_up_by_large_scheduled_vehicle=self.train_lsv, - ) - - free_capacity_in_teu = self.lsv_repository.get_free_capacity_for_outbound_journey(self.train) - self.assertEqual(free_capacity_in_teu, 2) - - def test_free_capacity_for_one_ffe(self): - Container.create( - weight=20, - length=ContainerLength.forty_feet, - storage_requirement=StorageRequirement.standard, - delivered_by=ModeOfTransport.truck, - picked_up_by=ModeOfTransport.train, - picked_up_by_initial=ModeOfTransport.train, - picked_up_by_large_scheduled_vehicle=self.train_lsv, - ) - - free_capacity_in_teu = self.lsv_repository.get_free_capacity_for_outbound_journey(self.train) - self.assertEqual(free_capacity_in_teu, 1) - - def test_free_capacity_for_45_foot_container(self): - Container.create( - weight=20, - length=ContainerLength.forty_five_feet, - storage_requirement=StorageRequirement.standard, - delivered_by=ModeOfTransport.truck, - picked_up_by=ModeOfTransport.train, - picked_up_by_initial=ModeOfTransport.feeder, - picked_up_by_large_scheduled_vehicle=self.train_lsv, - ) - - free_capacity_in_teu = self.lsv_repository.get_free_capacity_for_outbound_journey(self.train) - self.assertEqual(free_capacity_in_teu, 0.75) - - def test_free_capacity_for_other_container(self): - Container.create( - weight=20, - length=ContainerLength.other, - storage_requirement=StorageRequirement.standard, - delivered_by=ModeOfTransport.truck, - picked_up_by=ModeOfTransport.train, - picked_up_by_initial=ModeOfTransport.feeder, - picked_up_by_large_scheduled_vehicle=self.train_lsv, - ) - - free_capacity_in_teu = self.lsv_repository.get_free_capacity_for_outbound_journey(self.train) - self.assertEqual(free_capacity_in_teu, 0.5) diff --git a/conflowgen/tests/domain_models/reposistories/test_schedule_repository.py b/conflowgen/tests/domain_models/reposistories/test_schedule_repository.py index d926101a..bd323fee 100644 --- a/conflowgen/tests/domain_models/reposistories/test_schedule_repository.py +++ b/conflowgen/tests/domain_models/reposistories/test_schedule_repository.py @@ -2,6 +2,7 @@ import unittest import unittest.mock +from conflowgen.descriptive_datatypes import FlowDirection from conflowgen.domain_models.container import Container from conflowgen.domain_models.data_types.container_length import ContainerLength from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport @@ -36,7 +37,8 @@ def test_empty_schedule_database_throws_no_exception(self): datetime.datetime.now(), datetime.datetime.now() + datetime.timedelta(days=21), vehicle_type=ModeOfTransport.train, - required_capacity=ContainerLength.twenty_feet + required_capacity=ContainerLength.twenty_feet, + flow_direction=FlowDirection.undefined ) self.assertEqual(len(vehicles_and_frequency), 0) @@ -48,27 +50,26 @@ def test_find_vehicle_if_in_time_range(self): vehicle_arrives_at=datetime.date(year=2021, month=8, day=7), vehicle_arrives_at_time=datetime.time(hour=13, minute=15), average_vehicle_capacity=90, - average_moved_capacity=1, + average_inbound_container_volume=1, ) train_moves_this_capacity = 7 train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=90, - moved_capacity=train_moves_this_capacity, + inbound_container_volume=train_moves_this_capacity, scheduled_arrival=datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15), schedule=schedule ) - train_lsv.save() - train = Train.create( + Train.create( large_scheduled_vehicle=train_lsv ) - train.save() vehicles = self.schedule_repository.get_departing_vehicles( start=datetime.datetime(year=2021, month=8, day=5, hour=0, minute=0), end=datetime.datetime(year=2021, month=8, day=10, hour=23, minute=59), vehicle_type=ModeOfTransport.train, - required_capacity=ContainerLength.twenty_feet + required_capacity=ContainerLength.twenty_feet, + flow_direction=FlowDirection.undefined ) self.assertEqual(len(vehicles), 1) @@ -82,27 +83,26 @@ def test_export_buffer_below_capacity(self): vehicle_arrives_at=datetime.date(year=2021, month=8, day=7), vehicle_arrives_at_time=datetime.time(hour=13, minute=15), average_vehicle_capacity=90, - average_moved_capacity=1, + average_inbound_container_volume=1, ) train_moves_this_capacity = 7 train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=90, - moved_capacity=train_moves_this_capacity, + inbound_container_volume=train_moves_this_capacity, scheduled_arrival=datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15), schedule=schedule ) - train_lsv.save() - train = Train.create( + Train.create( large_scheduled_vehicle=train_lsv ) - train.save() vehicles = self.schedule_repository.get_departing_vehicles( start=datetime.datetime(year=2021, month=8, day=5, hour=0, minute=0), end=datetime.datetime(year=2021, month=8, day=10, hour=23, minute=59), vehicle_type=ModeOfTransport.train, - required_capacity=ContainerLength.twenty_feet + required_capacity=ContainerLength.twenty_feet, + flow_direction=FlowDirection.undefined ) self.assertEqual(len(vehicles), 1) @@ -115,27 +115,26 @@ def test_export_buffer_above_capacity(self): vehicle_arrives_at=datetime.date(year=2021, month=8, day=7), vehicle_arrives_at_time=datetime.time(hour=13, minute=15), average_vehicle_capacity=4, - average_moved_capacity=2, + average_inbound_container_volume=2, ) train_moves_this_capacity = 7 train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=8, - moved_capacity=train_moves_this_capacity, + inbound_container_volume=train_moves_this_capacity, scheduled_arrival=datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15), schedule=schedule ) - train_lsv.save() - train = Train.create( + Train.create( large_scheduled_vehicle=train_lsv ) - train.save() vehicles = self.schedule_repository.get_departing_vehicles( start=datetime.datetime(year=2021, month=8, day=5, hour=0, minute=0), end=datetime.datetime(year=2021, month=8, day=10, hour=23, minute=59), vehicle_type=ModeOfTransport.train, - required_capacity=ContainerLength.twenty_feet + required_capacity=ContainerLength.twenty_feet, + flow_direction=FlowDirection.undefined ) self.assertEqual(len(vehicles), 1) @@ -147,23 +146,23 @@ def test_ignore_vehicle_outside_time_range(self): vehicle_arrives_at=datetime.date(year=2021, month=8, day=7), vehicle_arrives_at_time=datetime.time(hour=13, minute=15), average_vehicle_capacity=90, - average_moved_capacity=1, + average_inbound_container_volume=1, ) train_moves_this_capacity = 7 - train = LargeScheduledVehicle.create( + LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=90, - moved_capacity=train_moves_this_capacity, + inbound_container_volume=train_moves_this_capacity, scheduled_arrival=datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15), schedule=schedule ) - train.save() vehicles_and_frequency = self.schedule_repository.get_departing_vehicles( start=datetime.datetime(year=2021, month=8, day=10, hour=13, minute=15), end=datetime.datetime(year=2021, month=8, day=15, hour=13, minute=15), vehicle_type=ModeOfTransport.train, - required_capacity=ContainerLength.forty_feet + required_capacity=ContainerLength.forty_feet, + flow_direction=FlowDirection.undefined ) self.assertEqual(len(vehicles_and_frequency), 0) @@ -175,24 +174,22 @@ def test_check_used_20_foot_containers(self): vehicle_arrives_at=datetime.date(year=2021, month=8, day=7), vehicle_arrives_at_time=datetime.time(hour=13, minute=15), average_vehicle_capacity=90, - average_moved_capacity=90, + average_inbound_container_volume=90, ) train_moves_this_capacity_in_teu = 2 train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=90, - moved_capacity=train_moves_this_capacity_in_teu, + inbound_container_volume=train_moves_this_capacity_in_teu, scheduled_arrival=datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15), schedule=schedule ) - train_lsv.save() train = Train.create( large_scheduled_vehicle=train_lsv ) - train.save() # This container is already loaded on the train - Container.create( + container = Container.create( weight=20, length=ContainerLength.twenty_feet, storage_requirement=StorageRequirement.standard, @@ -205,7 +202,8 @@ def test_check_used_20_foot_containers(self): start=datetime.datetime(year=2021, month=8, day=5, hour=0, minute=0), end=datetime.datetime(year=2021, month=8, day=10, hour=23, minute=59), vehicle_type=ModeOfTransport.train, - required_capacity=ContainerLength.twenty_feet + required_capacity=ContainerLength.twenty_feet, + flow_direction=container.flow_direction ) self.assertEqual(len(available_vehicles), 1) @@ -218,24 +216,22 @@ def test_check_used_40_foot_containers(self): vehicle_arrives_at=datetime.date(year=2021, month=8, day=7), vehicle_arrives_at_time=datetime.time(hour=13, minute=15), average_vehicle_capacity=90, - average_moved_capacity=90, + average_inbound_container_volume=90, ) train_moves_this_capacity_in_teu = 3 train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=90, - moved_capacity=train_moves_this_capacity_in_teu, + inbound_container_volume=train_moves_this_capacity_in_teu, scheduled_arrival=datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15), schedule=schedule ) - train_lsv.save() - train = Train.create( + Train.create( large_scheduled_vehicle=train_lsv ) - train.save() # This container is already loaded on the train - Container.create( + container = Container.create( weight=20, length=ContainerLength.forty_feet, storage_requirement=StorageRequirement.standard, @@ -248,36 +244,35 @@ def test_check_used_40_foot_containers(self): start=datetime.datetime(year=2021, month=8, day=5, hour=0, minute=0), end=datetime.datetime(year=2021, month=8, day=10, hour=23, minute=59), vehicle_type=ModeOfTransport.train, - required_capacity=ContainerLength.twenty_feet + required_capacity=ContainerLength.twenty_feet, + flow_direction=container.flow_direction ) self.assertEqual(len(vehicles), 1) - def test_use_lsv_repository(self): + def test_use_vehicle_capacity_manager(self): schedule = Schedule.create( vehicle_type=ModeOfTransport.train, service_name="TestService", vehicle_arrives_at=datetime.date(year=2021, month=8, day=7), vehicle_arrives_at_time=datetime.time(hour=13, minute=15), average_vehicle_capacity=90, - average_moved_capacity=90, + average_inbound_container_volume=90, ) train_moves_this_capacity_in_teu = 3 train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=90, - moved_capacity=train_moves_this_capacity_in_teu, + inbound_container_volume=train_moves_this_capacity_in_teu, scheduled_arrival=datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15), schedule=schedule ) - train_lsv.save() train = Train.create( large_scheduled_vehicle=train_lsv ) - train.save() # This container is already loaded on the train - Container.create( + container = Container.create( weight=20, length=ContainerLength.forty_feet, storage_requirement=StorageRequirement.standard, @@ -288,13 +283,27 @@ def test_use_lsv_repository(self): ) with unittest.mock.patch.object( - self.schedule_repository.large_scheduled_vehicle_repository, + self.schedule_repository.vehicle_capacity_manager, 'get_free_capacity_for_outbound_journey', return_value=1) as mock_method: self.schedule_repository.get_departing_vehicles( start=datetime.datetime(year=2021, month=8, day=5, hour=0, minute=0), end=datetime.datetime(year=2021, month=8, day=10, hour=23, minute=59), vehicle_type=ModeOfTransport.train, - required_capacity=ContainerLength.twenty_feet + required_capacity=ContainerLength.twenty_feet, + flow_direction=container.flow_direction ) - mock_method.assert_called_once_with(train) + mock_method.assert_called_once_with(train, FlowDirection.undefined) + + def test_set_ramp_up_and_down_period(self): + with unittest.mock.patch.object( + self.schedule_repository.vehicle_capacity_manager, + 'set_ramp_up_and_down_times', + return_value=None) as mock_method: + self.schedule_repository.set_ramp_up_and_down_times( + datetime.datetime(2023, 1, 1), datetime.datetime(2024, 1, 1) + ) + mock_method.assert_called_once_with( + ramp_up_period_end=datetime.datetime(2023, 1, 1), + ramp_down_period_start=datetime.datetime(2024, 1, 1) + ) diff --git a/conflowgen/tests/domain_models/test_container.py b/conflowgen/tests/domain_models/test_container.py index 369f65cd..1ac20569 100644 --- a/conflowgen/tests/domain_models/test_container.py +++ b/conflowgen/tests/domain_models/test_container.py @@ -5,8 +5,10 @@ import unittest from dataclasses import dataclass +import parameterized from peewee import IntegrityError +from conflowgen.descriptive_datatypes import FlowDirection from conflowgen.domain_models.container import Container, FaultyDataException, NoPickupVehicleException from conflowgen.domain_models.data_types.container_length import ContainerLength from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport @@ -19,6 +21,7 @@ class TestContainer(unittest.TestCase): """ Rudimentarily check if peewee can handle container entries in the database. + Also check whether the class properties work. """ def setUp(self) -> None: @@ -150,3 +153,63 @@ def __init__(self, value: int, name: str): with self.assertRaises(NoPickupVehicleException): container.get_departure_time() + + @parameterized.parameterized.expand([ + [ContainerLength.twenty_feet, 1], + [ContainerLength.forty_feet, 2], + [ContainerLength.forty_five_feet, 2.25], + [ContainerLength.other, 2.5] + ]) + def test_occupied_teu(self, container_size, teu): + """Test whether the container size is correctly converted to TEU""" + container = Container.create( + weight=10, + delivered_by=ModeOfTransport.barge, + picked_up_by=ModeOfTransport.truck, + picked_up_by_initial=ModeOfTransport.deep_sea_vessel, + length=container_size, + storage_requirement=StorageRequirement.standard + ) + self.assertEqual(teu, container.occupied_teu) + + @parameterized.parameterized.expand([ + [ModeOfTransport.deep_sea_vessel, ModeOfTransport.deep_sea_vessel, FlowDirection.transshipment_flow], + [ModeOfTransport.deep_sea_vessel, ModeOfTransport.feeder, FlowDirection.transshipment_flow], + [ModeOfTransport.feeder, ModeOfTransport.deep_sea_vessel, FlowDirection.transshipment_flow], + [ModeOfTransport.feeder, ModeOfTransport.feeder, FlowDirection.transshipment_flow], + + [ModeOfTransport.deep_sea_vessel, ModeOfTransport.truck, FlowDirection.import_flow], + [ModeOfTransport.deep_sea_vessel, ModeOfTransport.barge, FlowDirection.import_flow], + [ModeOfTransport.deep_sea_vessel, ModeOfTransport.train, FlowDirection.import_flow], + [ModeOfTransport.feeder, ModeOfTransport.truck, FlowDirection.import_flow], + [ModeOfTransport.feeder, ModeOfTransport.barge, FlowDirection.import_flow], + [ModeOfTransport.feeder, ModeOfTransport.train, FlowDirection.import_flow], + + [ModeOfTransport.truck, ModeOfTransport.deep_sea_vessel, FlowDirection.export_flow], + [ModeOfTransport.truck, ModeOfTransport.feeder, FlowDirection.export_flow], + [ModeOfTransport.barge, ModeOfTransport.deep_sea_vessel, FlowDirection.export_flow], + [ModeOfTransport.barge, ModeOfTransport.feeder, FlowDirection.export_flow], + [ModeOfTransport.train, ModeOfTransport.deep_sea_vessel, FlowDirection.export_flow], + [ModeOfTransport.train, ModeOfTransport.feeder, FlowDirection.export_flow], + + [ModeOfTransport.truck, ModeOfTransport.truck, FlowDirection.undefined], + [ModeOfTransport.truck, ModeOfTransport.barge, FlowDirection.undefined], + [ModeOfTransport.truck, ModeOfTransport.train, FlowDirection.undefined], + [ModeOfTransport.barge, ModeOfTransport.truck, FlowDirection.undefined], + [ModeOfTransport.barge, ModeOfTransport.barge, FlowDirection.undefined], + [ModeOfTransport.barge, ModeOfTransport.train, FlowDirection.undefined], + [ModeOfTransport.train, ModeOfTransport.truck, FlowDirection.undefined], + [ModeOfTransport.train, ModeOfTransport.barge, FlowDirection.undefined], + [ModeOfTransport.train, ModeOfTransport.train, FlowDirection.undefined], + ]) + def test_flow_direction(self, delivered_by, picked_up_by, container_flow): + """Test whether all flow directions are detected correctly""" + container: Container = Container.create( + weight=10, + delivered_by=delivered_by, + picked_up_by=picked_up_by, + picked_up_by_initial=ModeOfTransport.truck, + length=ContainerLength.forty_feet, + storage_requirement=StorageRequirement.standard + ) + self.assertEqual(container_flow, container.flow_direction) diff --git a/conflowgen/tests/domain_models/test_vehicle.py b/conflowgen/tests/domain_models/test_vehicle.py index ee046b56..2d11790b 100644 --- a/conflowgen/tests/domain_models/test_vehicle.py +++ b/conflowgen/tests/domain_models/test_vehicle.py @@ -74,12 +74,12 @@ def test_save_feeder_to_database(self) -> None: vehicle_type=ModeOfTransport.feeder, vehicle_arrives_at=now, average_vehicle_capacity=1100, - average_moved_capacity=200 + average_inbound_container_volume=200 ) lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=1000, - moved_capacity=200, + inbound_container_volume=200, scheduled_arrival=now, schedule=schedule ) @@ -95,12 +95,12 @@ def test_repr(self) -> None: vehicle_type=ModeOfTransport.feeder, vehicle_arrives_at=now, average_vehicle_capacity=1100, - average_moved_capacity=200 + average_inbound_container_volume=200 ) lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=1000, - moved_capacity=200, + inbound_container_volume=200, scheduled_arrival=now, schedule=schedule ) @@ -132,12 +132,12 @@ def test_save_barge_to_database(self) -> None: vehicle_type=ModeOfTransport.barge, vehicle_arrives_at=now, average_vehicle_capacity=1100, - average_moved_capacity=200 + average_inbound_container_volume=200 ) lsv = LargeScheduledVehicle.create( vehicle_name="TestBarge1", capacity_in_teu=1000, - moved_capacity=200, + inbound_container_volume=200, scheduled_arrival=now, schedule=schedule ) @@ -153,12 +153,12 @@ def test_repr(self) -> None: vehicle_type=ModeOfTransport.barge, vehicle_arrives_at=now, average_vehicle_capacity=1100, - average_moved_capacity=200 + average_inbound_container_volume=200 ) lsv = LargeScheduledVehicle.create( vehicle_name="TestBarge1", capacity_in_teu=1000, - moved_capacity=200, + inbound_container_volume=200, scheduled_arrival=now, schedule=schedule ) @@ -177,13 +177,13 @@ def test_get_mode_of_transport(self) -> None: service_name="MyTestBargeLine", vehicle_type=ModeOfTransport.barge, vehicle_arrives_at=now, - average_vehicle_capacity=1100, - average_moved_capacity=200 + average_vehicle_capacity=110, + average_inbound_container_volume=100 ) lsv = LargeScheduledVehicle.create( vehicle_name="TestBarge1", - capacity_in_teu=1000, - moved_capacity=200, + capacity_in_teu=110, + inbound_container_volume=100, scheduled_arrival=now, schedule=schedule ) diff --git a/conflowgen/tests/flow_generator/test_allocate_space_for_containers_delivered_by_truck_service.py b/conflowgen/tests/flow_generator/test_allocate_space_for_containers_delivered_by_truck_service.py index 4e749554..8f5bbbb8 100644 --- a/conflowgen/tests/flow_generator/test_allocate_space_for_containers_delivered_by_truck_service.py +++ b/conflowgen/tests/flow_generator/test_allocate_space_for_containers_delivered_by_truck_service.py @@ -69,21 +69,18 @@ def _create_feeder(scheduled_arrival: datetime.datetime) -> Feeder: vehicle_arrives_at=scheduled_arrival.date(), vehicle_arrives_at_time=scheduled_arrival.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) - schedule.save() feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=scheduled_arrival, schedule=schedule ) - feeder_lsv.save() feeder = Feeder.create( large_scheduled_vehicle=feeder_lsv ) - feeder.save() return feeder @staticmethod @@ -94,21 +91,18 @@ def _create_train(scheduled_arrival: datetime.datetime) -> Train: vehicle_arrives_at=scheduled_arrival.date(), vehicle_arrives_at_time=scheduled_arrival.time(), average_vehicle_capacity=90, - average_moved_capacity=90, + average_inbound_container_volume=90, ) - schedule.save() train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=96, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=scheduled_arrival, schedule=schedule ) - train_lsv.save() train = Train.create( large_scheduled_vehicle=train_lsv ) - train.save() return train @staticmethod @@ -151,7 +145,6 @@ def _create_container_for_large_scheduled_vehicle(vehicle: AbstractLargeSchedule picked_up_by=ModeOfTransport.feeder, picked_up_by_initial=ModeOfTransport.feeder ) - container.save() return container def test_does_nothing_if_no_vehicle_is_available(self): diff --git a/conflowgen/tests/flow_generator/test_assign_destination_to_container_service.py b/conflowgen/tests/flow_generator/test_assign_destination_to_container_service.py index 6818e4c7..c7f9717a 100644 --- a/conflowgen/tests/flow_generator/test_assign_destination_to_container_service.py +++ b/conflowgen/tests/flow_generator/test_assign_destination_to_container_service.py @@ -48,12 +48,12 @@ def _create_feeder(scheduled_arrival: datetime.datetime) -> Feeder: vehicle_arrives_at=scheduled_arrival.date(), vehicle_arrives_at_time=scheduled_arrival.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=scheduled_arrival, schedule=schedule ) @@ -70,12 +70,12 @@ def _create_train(scheduled_arrival: datetime.datetime) -> Train: vehicle_arrives_at=scheduled_arrival.date(), vehicle_arrives_at_time=scheduled_arrival.time(), average_vehicle_capacity=90, - average_moved_capacity=90, + average_inbound_container_volume=90, ) train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=96, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=scheduled_arrival, schedule=schedule ) diff --git a/conflowgen/tests/flow_generator/test_container_flow_generator_service__generate.py b/conflowgen/tests/flow_generator/test_container_flow_generator_service__generate.py index 1bb2ded2..fbdb1d8b 100644 --- a/conflowgen/tests/flow_generator/test_container_flow_generator_service__generate.py +++ b/conflowgen/tests/flow_generator/test_container_flow_generator_service__generate.py @@ -7,6 +7,7 @@ from conflowgen.application.repositories.container_flow_generation_properties_repository import \ ContainerFlowGenerationPropertiesRepository from conflowgen.database_connection.create_tables import create_tables +from conflowgen.domain_models.container import Container from conflowgen.domain_models.distribution_models.container_dwell_time_distribution import \ ContainerDwellTimeDistribution from conflowgen.domain_models.distribution_models.mode_of_transport_distribution import ModeOfTransportDistribution @@ -49,22 +50,22 @@ def test_happy_path_no_mocking(self): create_tables(self.sqlite_db) seed_all_distributions() port_call_manager = PortCallManager() - port_call_manager.add_vehicle( + port_call_manager.add_service_that_calls_terminal( vehicle_type=ModeOfTransport.feeder, service_name="TestFeeder", vehicle_arrives_at=datetime.date(2021, 7, 9), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=100, + average_inbound_container_volume=100, next_destinations=None ) - port_call_manager.add_vehicle( + port_call_manager.add_service_that_calls_terminal( vehicle_type=ModeOfTransport.deep_sea_vessel, service_name="TestDeepSeaVessel", vehicle_arrives_at=datetime.date(2021, 7, 9), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=100, + average_inbound_container_volume=100, next_destinations=None ) self.container_flow_generator_service.generate() @@ -73,3 +74,61 @@ def test_nothing_to_do(self): create_tables(self.sqlite_db) seed_all_distributions() self.container_flow_generator_service.generate() + + def test_happy_path_no_mocking_with_ramp_up_and_ramp_down(self): + create_tables(self.sqlite_db) + seed_all_distributions() + + container_flow_generation_properties_manager = ContainerFlowGenerationPropertiesRepository() + properties: ContainerFlowGenerationProperties = (container_flow_generation_properties_manager + .get_container_flow_generation_properties()) + properties.ramp_up_period = 5 + properties.ramp_down_period = 5 + container_flow_generation_properties_manager.set_container_flow_generation_properties(properties) + + port_call_manager = PortCallManager() + port_call_manager.add_service_that_calls_terminal( + vehicle_type=ModeOfTransport.feeder, + service_name="TestFeeder", + vehicle_arrives_at=properties.start_date + datetime.timedelta(days=3), + vehicle_arrives_at_time=datetime.time(11), + average_vehicle_capacity=800, + average_inbound_container_volume=50, + next_destinations=None + ) + port_call_manager.add_service_that_calls_terminal( + vehicle_type=ModeOfTransport.deep_sea_vessel, + service_name="TestDeepSeaVessel2", + vehicle_arrives_at=properties.end_date - datetime.timedelta(days=2), + vehicle_arrives_at_time=datetime.time(11), + average_vehicle_capacity=12000, + average_inbound_container_volume=100, + next_destinations=None + ) + self.container_flow_generator_service.generate() + + # Vehicle 1 - inbound during ramp-up untouched + number_containers_during_ramp_up = Container.select().where( + Container.delivered_by == ModeOfTransport.feeder + ).count() + self.assertLess( + number_containers_during_ramp_up, + 100 + ) + self.assertGreater( + number_containers_during_ramp_up, + 50 + ) + + # Vehicle 2 - inbound volume during ramp-down throttled + number_containers_during_ramp_down = Container.select().where( + Container.delivered_by == ModeOfTransport.deep_sea_vessel + ).count() + self.assertLess( + number_containers_during_ramp_down, + 150 + ) + self.assertGreater( + number_containers_during_ramp_down, + 50 + ) diff --git a/conflowgen/tests/flow_generator/test_export_container_flow_service__container.py b/conflowgen/tests/flow_generator/test_export_container_flow_service__container.py index 3963a612..efdd5c5d 100644 --- a/conflowgen/tests/flow_generator/test_export_container_flow_service__container.py +++ b/conflowgen/tests/flow_generator/test_export_container_flow_service__container.py @@ -113,7 +113,7 @@ def test_convert_table_to_pandas_dataframe_with_container_with_destination(self) vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) destination = Destination.create( diff --git a/conflowgen/tests/flow_generator/test_large_scheduled_vehicle_for_onward_transportation_manager.py b/conflowgen/tests/flow_generator/test_large_scheduled_vehicle_for_onward_transportation_manager.py index 55286a7b..c89dc849 100644 --- a/conflowgen/tests/flow_generator/test_large_scheduled_vehicle_for_onward_transportation_manager.py +++ b/conflowgen/tests/flow_generator/test_large_scheduled_vehicle_for_onward_transportation_manager.py @@ -68,21 +68,18 @@ def _create_feeder( vehicle_arrives_at=scheduled_arrival.date(), vehicle_arrives_at_time=scheduled_arrival.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, ) - schedule.save() feeder_lsv = LargeScheduledVehicle.create( vehicle_name="TestFeeder1", capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=scheduled_arrival, schedule=schedule ) - feeder_lsv.save() feeder = Feeder.create( large_scheduled_vehicle=feeder_lsv ) - feeder.save() return feeder @staticmethod @@ -93,12 +90,12 @@ def _create_train(scheduled_arrival: datetime.datetime, service_suffix: str = "" vehicle_arrives_at=scheduled_arrival.date(), vehicle_arrives_at_time=scheduled_arrival.time(), average_vehicle_capacity=90, - average_moved_capacity=90, + average_inbound_container_volume=90, ) train_lsv = LargeScheduledVehicle.create( vehicle_name="TestTrain1", capacity_in_teu=96, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=scheduled_arrival, schedule=schedule ) @@ -170,7 +167,7 @@ def test_load_container_from_feeder_to_feeder(self): def test_do_not_overload_feeder_with_truck_traffic(self): truck = self._create_truck(datetime.datetime(year=2021, month=8, day=5, hour=9, minute=0)) feeder = self._create_feeder(datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15)) - feeder.large_scheduled_vehicle.moved_capacity = 10 # in TEU + feeder.large_scheduled_vehicle.inbound_container_volume = 10 # in TEU containers = [self._create_container_for_truck(truck) for _ in range(10)] self.assertEqual(Container.select().count(), 10) teu_generated = sum((ContainerLength.get_teu_factor(container.length) for container in containers)) @@ -194,11 +191,11 @@ def test_do_not_overload_feeder_with_train_traffic(self): train = self._create_train(datetime.datetime(year=2021, month=8, day=5, hour=9, minute=0)) containers = [ self._create_container_for_large_scheduled_vehicle(train) - for _ in range(train.large_scheduled_vehicle.moved_capacity) # here only 20' containers + for _ in range(train.large_scheduled_vehicle.inbound_container_volume) # here only 20' containers ] feeder = self._create_feeder(datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15)) - feeder.large_scheduled_vehicle.moved_capacity = 80 # in TEU + feeder.large_scheduled_vehicle.inbound_container_volume = 80 # in TEU feeder.save() self.assertEqual(Container.select().count(), 90) @@ -222,11 +219,11 @@ def test_do_not_load_if_the_time_span_is_too_long(self): train = self._create_train(datetime.datetime(year=2021, month=8, day=5, hour=9, minute=0)) containers = [ self._create_container_for_large_scheduled_vehicle(train) - for _ in range(train.large_scheduled_vehicle.moved_capacity) # here only 20' containers + for _ in range(train.large_scheduled_vehicle.inbound_container_volume) # here only 20' containers ] feeder = self._create_feeder(datetime.datetime(year=2022, month=8, day=7, hour=13, minute=15)) - feeder.large_scheduled_vehicle.moved_capacity = 80 # in TEU + feeder.large_scheduled_vehicle.inbound_container_volume = 80 # in TEU feeder.save() self.assertEqual(Container.select().count(), 90) @@ -245,16 +242,16 @@ def test_do_not_overload_feeder_with_train_traffic_of_two_vehicles(self): train_2 = self._create_train(datetime.datetime(year=2021, month=8, day=5, hour=15, minute=0), "2") containers_1 = [ self._create_container_for_large_scheduled_vehicle(train_1) - for _ in range(train_1.large_scheduled_vehicle.moved_capacity) # here only 20' containers + for _ in range(train_1.large_scheduled_vehicle.inbound_container_volume) # here only 20' containers ] containers_2 = [ self._create_container_for_large_scheduled_vehicle(train_2) - for _ in range(train_2.large_scheduled_vehicle.moved_capacity) # here only 20' containers + for _ in range(train_2.large_scheduled_vehicle.inbound_container_volume) # here only 20' containers ] containers = containers_1 + containers_2 feeder = self._create_feeder(datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15)) - feeder.large_scheduled_vehicle.moved_capacity = 80 # in TEU + feeder.large_scheduled_vehicle.inbound_container_volume = 80 # in TEU feeder.save() self.assertEqual(Container.select().count(), 180) @@ -279,11 +276,11 @@ def test_do_not_overload_feeder_with_train_traffic_of_two_vehicles_and_changing_ train_2 = self._create_train(datetime.datetime(year=2021, month=8, day=5, hour=15, minute=0), "2") containers_1 = [ self._create_container_for_large_scheduled_vehicle(train_1) - for _ in range(train_1.large_scheduled_vehicle.moved_capacity) # here only 20' containers + for _ in range(train_1.large_scheduled_vehicle.inbound_container_volume) # here only 20' containers ] containers_2 = [ self._create_container_for_large_scheduled_vehicle(train_2) - for _ in range(train_2.large_scheduled_vehicle.moved_capacity) # here only 20' containers + for _ in range(train_2.large_scheduled_vehicle.inbound_container_volume) # here only 20' containers ] for container in containers_2: container.length = ContainerLength.forty_feet @@ -291,7 +288,7 @@ def test_do_not_overload_feeder_with_train_traffic_of_two_vehicles_and_changing_ containers = containers_1 + containers_2 feeder = self._create_feeder(datetime.datetime(year=2021, month=8, day=7, hour=13, minute=15)) - feeder.large_scheduled_vehicle.moved_capacity = 80 # in TEU + feeder.large_scheduled_vehicle.inbound_container_volume = 80 # in TEU feeder.save() self.assertEqual(Container.select().count(), 180) @@ -316,3 +313,48 @@ def test_nothing_to_do(self): self.manager.schedule_repository, "get_departing_vehicles", return_value=None) as get_vehicles_method: self.manager.choose_departing_vehicle_for_containers() get_vehicles_method.assert_not_called() + + def test_behavior_during_ramp_up_period(self): + """During ramp-up, the capacity of vessels is artificially reduced""" + + # the + feeder_1 = self._create_feeder( + datetime.datetime(year=2022, month=8, day=7, hour=13, minute=15), "1" + ) + + feeder_2 = self._create_feeder( + datetime.datetime(year=2021, month=8, day=10, hour=15, minute=0), "2" + ) + + feeder_1.large_scheduled_vehicle.inbound_container_volume = 100 # in TEU + feeder_1.save() + + containers = [ + self._create_container_for_large_scheduled_vehicle(feeder_1) + for _ in range(feeder_1.large_scheduled_vehicle.inbound_container_volume) # here only 20' containers + ] + + self.manager.reload_properties( + transportation_buffer=0, + ramp_up_period_end=datetime.date(2021, 8, 12) + ) + + # run actual function + self.manager.choose_departing_vehicle_for_containers() + + containers_reloaded: Iterable[Container] = Container.select().where( + Container.picked_up_by_large_scheduled_vehicle == feeder_2 + ) + self.assertTrue(set(containers_reloaded).issubset(set(containers)), "Feeder must only load generated " + "containers") + + teu_loaded = 0 + for container in containers_reloaded: # pylint: disable=not-an-iterable + self.assertEqual(container.picked_up_by_large_scheduled_vehicle, feeder_2.large_scheduled_vehicle) + teu_loaded += ContainerLength.get_teu_factor(container.length) + self.assertLessEqual(teu_loaded, 30, "Feeder must have loaded much less containers because this is the" + "ramp-up period!") + + def test_behavior_during_ramp_down_period(self): + """During ramp-down, transshipment flows should not be affected!""" + ... # TODO! diff --git a/conflowgen/tests/flow_generator/test_truck_for_export_containers_manager.py b/conflowgen/tests/flow_generator/test_truck_for_export_containers_manager.py index 4bb684ce..02043455 100644 --- a/conflowgen/tests/flow_generator/test_truck_for_export_containers_manager.py +++ b/conflowgen/tests/flow_generator/test_truck_for_export_containers_manager.py @@ -255,12 +255,12 @@ def test_happy_path(self): vehicle_arrives_at=container_arrival_time.date(), vehicle_arrives_at_time=container_arrival_time.time(), average_vehicle_capacity=5000, - average_moved_capacity=1200, + average_inbound_container_volume=1200, ) lsv = LargeScheduledVehicle.create( vehicle_name="TestDeepSeaVessel", capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=container_arrival_time, schedule=schedule ) diff --git a/conflowgen/tests/flow_generator/test_truck_for_import_containers_manager.py b/conflowgen/tests/flow_generator/test_truck_for_import_containers_manager.py index 515cbf47..a534655e 100644 --- a/conflowgen/tests/flow_generator/test_truck_for_import_containers_manager.py +++ b/conflowgen/tests/flow_generator/test_truck_for_import_containers_manager.py @@ -275,12 +275,12 @@ def test_happy_path(self): vehicle_arrives_at=container_arrival_time.date(), vehicle_arrives_at_time=container_arrival_time.time(), average_vehicle_capacity=5000, - average_moved_capacity=1200, + average_inbound_container_volume=1200, ) lsv = LargeScheduledVehicle.create( vehicle_name="TestDeepSeaVessel", capacity_in_teu=schedule.average_vehicle_capacity, - moved_capacity=schedule.average_moved_capacity, + inbound_container_volume=schedule.average_inbound_container_volume, scheduled_arrival=container_arrival_time, schedule=schedule ) diff --git a/conflowgen/tests/notebooks/analyses_with_missing_data.ipynb b/conflowgen/tests/notebooks/analyses_with_missing_data.ipynb index 8bf2826e..4c03414a 100644 --- a/conflowgen/tests/notebooks/analyses_with_missing_data.ipynb +++ b/conflowgen/tests/notebooks/analyses_with_missing_data.ipynb @@ -197,7 +197,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.1" + "version": "3.9.7" } }, "nbformat": 4, diff --git a/conflowgen/tests/previews/test_container_flow_by_vehicle_type_preview.py b/conflowgen/tests/previews/test_container_flow_by_vehicle_type_preview.py index 77fd5011..7b2b02a0 100644 --- a/conflowgen/tests/previews/test_container_flow_by_vehicle_type_preview.py +++ b/conflowgen/tests/previews/test_container_flow_by_vehicle_type_preview.py @@ -99,7 +99,7 @@ def test_with_single_arrival_schedules(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) schedule.save() diff --git a/conflowgen/tests/previews/test_container_flow_by_vehicle_type_preview_report.py b/conflowgen/tests/previews/test_container_flow_by_vehicle_type_preview_report.py index 3734cf90..93aed9ab 100644 --- a/conflowgen/tests/previews/test_container_flow_by_vehicle_type_preview_report.py +++ b/conflowgen/tests/previews/test_container_flow_by_vehicle_type_preview_report.py @@ -123,7 +123,7 @@ def test_inbound_with_single_arrival_schedules(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) actual_report = self.preview_report.get_report_as_text() @@ -172,7 +172,7 @@ def test_report_with_schedules_as_graph(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) fig = self.preview_report.get_report_as_graph() 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..cb45034d 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] @@ -96,7 +96,7 @@ def test_inbound_with_single_arrival_schedules(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) capacity_with_one_feeder = self.preview.get_inbound_capacity_of_vehicles().teu @@ -124,7 +124,7 @@ def test_inbound_with_several_arrivals_schedules(self): vehicle_arrives_at=two_days_later.date(), vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300 + average_inbound_container_volume=300 ) capacity_with_one_feeder = self.preview.get_inbound_capacity_of_vehicles().teu self.assertSetEqual(set(ModeOfTransport), set(capacity_with_one_feeder.keys())) @@ -151,7 +151,7 @@ def test_outbound_average_capacity_with_several_arrivals_schedules(self): vehicle_arrives_at=two_days_later.date(), vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300 + average_inbound_container_volume=300 ) capacities = self.preview.get_outbound_capacity_of_vehicles() capacity_with_one_feeder = capacities.used.teu @@ -183,7 +183,7 @@ def test_outbound_maximum_capacity_with_several_arrivals_schedules(self): vehicle_arrives_at=two_days_later.date(), vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300 + average_inbound_container_volume=300 ) capacity_with_one_feeder = self.preview.get_outbound_capacity_of_vehicles().maximum.teu self.assertSetEqual(set(ModeOfTransport), set(capacity_with_one_feeder.keys())) diff --git a/conflowgen/tests/previews/test_inbound_and_outbound_vehicle_capacity_preview_report.py b/conflowgen/tests/previews/test_inbound_and_outbound_vehicle_capacity_preview_report.py index 0148dbaf..6963d7c6 100644 --- a/conflowgen/tests/previews/test_inbound_and_outbound_vehicle_capacity_preview_report.py +++ b/conflowgen/tests/previews/test_inbound_and_outbound_vehicle_capacity_preview_report.py @@ -103,7 +103,7 @@ def test_inbound_with_single_arrival_schedules(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) actual_report = self.preview_report.get_report_as_text() @@ -132,7 +132,7 @@ def test_report_with_schedules_as_graph(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) fig = self.preview_report.get_report_as_graph() diff --git a/conflowgen/tests/previews/test_modal_split_preview__get_modal_split_for_hinterland.py b/conflowgen/tests/previews/test_modal_split_preview__get_modal_split_for_hinterland.py index ea168ae4..33be55e0 100644 --- a/conflowgen/tests/previews/test_modal_split_preview__get_modal_split_for_hinterland.py +++ b/conflowgen/tests/previews/test_modal_split_preview__get_modal_split_for_hinterland.py @@ -97,7 +97,7 @@ def test_with_single_arrival_schedules(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) schedule.save() diff --git a/conflowgen/tests/previews/test_modal_split_preview__get_transshipment.py b/conflowgen/tests/previews/test_modal_split_preview__get_transshipment.py index f6950b48..3a59b87e 100644 --- a/conflowgen/tests/previews/test_modal_split_preview__get_transshipment.py +++ b/conflowgen/tests/previews/test_modal_split_preview__get_transshipment.py @@ -92,7 +92,7 @@ def test_with_single_arrival_schedules(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) actual_split = self.preview.get_transshipment_and_hinterland_split() diff --git a/conflowgen/tests/previews/test_modal_split_preview_report.py b/conflowgen/tests/previews/test_modal_split_preview_report.py index 0c84cb43..60359156 100644 --- a/conflowgen/tests/previews/test_modal_split_preview_report.py +++ b/conflowgen/tests/previews/test_modal_split_preview_report.py @@ -113,7 +113,7 @@ def test_inbound_with_single_arrival_schedules(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) actual_report = self.preview_report.get_report_as_text() @@ -154,7 +154,7 @@ def test_report_with_schedules_as_graph(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) axes = self.preview_report.get_report_as_graph() diff --git a/conflowgen/tests/previews/test_quay_side_throughput_preview.py b/conflowgen/tests/previews/test_quay_side_throughput_preview.py index fa070c1c..4bbd353f 100644 --- a/conflowgen/tests/previews/test_quay_side_throughput_preview.py +++ b/conflowgen/tests/previews/test_quay_side_throughput_preview.py @@ -103,7 +103,7 @@ def test_one_feeder(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=150, + average_inbound_container_volume=150, vehicle_arrives_every_k_days=-1 ) volume = self.preview.get_quay_side_throughput() diff --git a/conflowgen/tests/previews/test_quay_side_throughput_preview_report.py b/conflowgen/tests/previews/test_quay_side_throughput_preview_report.py index 7e89322d..a5622db6 100644 --- a/conflowgen/tests/previews/test_quay_side_throughput_preview_report.py +++ b/conflowgen/tests/previews/test_quay_side_throughput_preview_report.py @@ -92,7 +92,7 @@ def test_report_with_schedules_as_graph(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) axes = self.preview_report.get_report_as_graph() @@ -108,7 +108,7 @@ def test_text_report(self): vehicle_arrives_every_k_days=-1, vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=24000, - average_moved_capacity=24000 + average_inbound_container_volume=24000 ) report = self.preview_report.get_report_as_text() # flake8: noqa: W291 (ignore trailing whitespace in text report) diff --git a/conflowgen/tests/previews/test_truck_gate_throughput_preview.py b/conflowgen/tests/previews/test_truck_gate_throughput_preview.py index 52447103..ba8fd2cf 100644 --- a/conflowgen/tests/previews/test_truck_gate_throughput_preview.py +++ b/conflowgen/tests/previews/test_truck_gate_throughput_preview.py @@ -97,7 +97,7 @@ def test_get_total_trucks(self): vehicle_arrives_every_k_days=-1, vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300 + average_inbound_container_volume=300 ) # pylint: disable=protected-access total_trucks = self.preview._get_total_trucks() @@ -118,7 +118,7 @@ def test_get_weekly_trucks(self): vehicle_arrives_every_k_days=-1, vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300 + average_inbound_container_volume=300 ) weekly_trucks = self.preview._get_number_of_trucks_per_week() # 60 trucks total (from test_get_total_trucks above) @@ -137,7 +137,7 @@ def test_get_truck_distribution(self): vehicle_arrives_every_k_days=-1, vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300 + average_inbound_container_volume=300 ) weekly_truck_distribution = self.preview.get_weekly_truck_arrivals(True, False) self.assertEqual(weekly_truck_distribution, {3: 6, 4: 24}) diff --git a/conflowgen/tests/previews/test_truck_gate_throughput_preview_report.py b/conflowgen/tests/previews/test_truck_gate_throughput_preview_report.py index f1460e93..f94d173b 100644 --- a/conflowgen/tests/previews/test_truck_gate_throughput_preview_report.py +++ b/conflowgen/tests/previews/test_truck_gate_throughput_preview_report.py @@ -261,7 +261,7 @@ def test_report_with_schedule(self): vehicle_arrives_every_k_days=-1, vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=24000, - average_moved_capacity=24000 + average_inbound_container_volume=24000 ) report = self.preview_report.get_report_as_graph() self.assertIsNotNone(report) @@ -276,7 +276,7 @@ def test_text_report(self): vehicle_arrives_every_k_days=-1, vehicle_arrives_at_time=two_days_later.time(), average_vehicle_capacity=24000, - average_moved_capacity=24000 + average_inbound_container_volume=24000 ) report = self.preview_report.get_report_as_text() # flake8: noqa: W291 (ignore trailing whitespace in text report) diff --git a/conflowgen/tests/previews/test_vehicle_capacity_exceeded_preview.py b/conflowgen/tests/previews/test_vehicle_capacity_exceeded_preview.py index 6223bd39..1f6e4b59 100644 --- a/conflowgen/tests/previews/test_vehicle_capacity_exceeded_preview.py +++ b/conflowgen/tests/previews/test_vehicle_capacity_exceeded_preview.py @@ -113,7 +113,7 @@ def test_with_single_arrival_schedules(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=300, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) schedule.save() diff --git a/conflowgen/tests/previews/test_vehicle_capacity_exceeded_preview_report.py b/conflowgen/tests/previews/test_vehicle_capacity_exceeded_preview_report.py index 48e3e5b9..3aae61d2 100644 --- a/conflowgen/tests/previews/test_vehicle_capacity_exceeded_preview_report.py +++ b/conflowgen/tests/previews/test_vehicle_capacity_exceeded_preview_report.py @@ -105,7 +105,7 @@ def test_inbound_with_single_arrival_schedules(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) actual_report = self.preview_report.get_report_as_text() @@ -134,7 +134,7 @@ def test_report_with_schedules_as_graph(self): vehicle_arrives_at=one_week_later.date(), vehicle_arrives_at_time=one_week_later.time(), average_vehicle_capacity=400, - average_moved_capacity=300, + average_inbound_container_volume=300, vehicle_arrives_every_k_days=-1 ) fig = self.preview_report.get_report_as_graph() diff --git a/docs/api.rst b/docs/api.rst index 68cde3e6..b0b58ea4 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -143,6 +143,12 @@ Running analyses .. autoclass:: conflowgen.ContainerFlowAdjustmentByVehicleTypeAnalysisSummaryReport :members: +.. autoclass:: conflowgen.ContainerFlowByVehicleInstanceAnalysis + :members: + +.. autoclass:: conflowgen.ContainerFlowByVehicleInstanceAnalysisReport + :members: + .. autoclass:: conflowgen.ContainerFlowByVehicleTypeAnalysis :members: @@ -155,10 +161,10 @@ Running analyses .. autoclass:: conflowgen.InboundAndOutboundVehicleCapacityAnalysisReport :members: -.. autoclass:: conflowgen.InboundToOutboundVehicleCapacityUtilizationAnalysis +.. autoclass:: conflowgen.OutboundToInboundVehicleCapacityUtilizationAnalysis :members: -.. autoclass:: conflowgen.InboundToOutboundVehicleCapacityUtilizationAnalysisReport +.. autoclass:: conflowgen.OutboundToInboundVehicleCapacityUtilizationAnalysisReport :members: .. autoclass:: conflowgen.ModalSplitAnalysis diff --git a/docs/background.rst b/docs/background.rst index 497fbc8e..0326e225 100644 --- a/docs/background.rst +++ b/docs/background.rst @@ -220,7 +220,8 @@ If you just need a BibTeX entry for your citation software, this one should do t doi = {10.1007/978-3-031-05359-7_11}, month = {2}, pages = {133--143}, - publisher = {Springer Cham}, + publisher = {Springer International Publishing}, + address = {Cham, CH}, series = {Lecture Notes in Logistics}, title = {Container Flow Generation for Maritime Container Terminals}, year = {2022} diff --git a/docs/notebooks/analyses.ipynb b/docs/notebooks/analyses.ipynb index 9b3ff56a..a686bd92 100644 --- a/docs/notebooks/analyses.ipynb +++ b/docs/notebooks/analyses.ipynb @@ -289,12 +289,12 @@ "metadata": {}, "outputs": [], "source": [ - "vehicle_capacity_utilization_report = (\n", - " conflowgen.InboundToOutboundVehicleCapacityUtilizationAnalysisReport()\n", + "outbound_to_inbound_vehicle_capacity_utilization_report = (\n", + " conflowgen.OutboundToInboundVehicleCapacityUtilizationAnalysisReport()\n", ")\n", "\n", "print(\n", - " vehicle_capacity_utilization_report.get_report_as_text(\n", + " outbound_to_inbound_vehicle_capacity_utilization_report.get_report_as_text(\n", " vehicle_type={\n", " conflowgen.ModeOfTransport.deep_sea_vessel,\n", " conflowgen.ModeOfTransport.feeder,\n", diff --git a/docs/notebooks/first_steps.ipynb b/docs/notebooks/first_steps.ipynb index 5ececb96..bcc31d36 100644 --- a/docs/notebooks/first_steps.ipynb +++ b/docs/notebooks/first_steps.ipynb @@ -209,7 +209,7 @@ }, "source": [ "By using\n", - ":meth:`.PortCallManager.add_vehicle`,\n", + ":meth:`.PortCallManager.add_service_that_calls_terminal`,\n", "we can define the attributes for our feeder service.\n", "\n", "- ``vehicle_type`` defines, that we deal with a feeder as the mode of transport.\n", @@ -221,7 +221,7 @@ " This parameter must be a :py:obj:`datetime.time`.\n", "- ``average_vehicle_capacity`` defines the average capacity of the vessels utilized on this line.\n", " Parameter must be :py:obj:`int` or :py:obj:`float`.\n", - "- ``average_moved_capacity`` sets the capacity which is in average moved between the feeder and the terminal at each call.\n", + "- ``average_inbound_container_volume`` sets the capacity which is in average moved between the feeder and the terminal at each call.\n", " Parameter must be :py:obj:`int` or :py:obj:`float`.\n", "- ``next_destinations`` can be set, consisting of name and frequency which can, e.g., be used as implication for storage and stacking problems.\n", " A list of name-frequency pairs is expected here." @@ -234,13 +234,13 @@ "metadata": {}, "outputs": [], "source": [ - "port_call_manager.add_vehicle(\n", + "port_call_manager.add_service_that_calls_terminal(\n", " vehicle_type=conflowgen.ModeOfTransport.feeder,\n", " service_name=feeder_service_name,\n", " vehicle_arrives_at=datetime.date(2021, 7, 9),\n", " vehicle_arrives_at_time=datetime.time(11),\n", " average_vehicle_capacity=800,\n", - " average_moved_capacity=100,\n", + " average_inbound_container_volume=100,\n", " next_destinations=[\n", " (\"DEBRV\", 0.4), # 40% of the containers go here...\n", " (\"RULED\", 0.6) # and the other 60% of the containers go here.\n", @@ -263,13 +263,13 @@ "metadata": {}, "outputs": [], "source": [ - "port_call_manager.add_vehicle(\n", + "port_call_manager.add_service_that_calls_terminal(\n", " vehicle_type=conflowgen.ModeOfTransport.train,\n", " service_name=\"JR03A\",\n", " vehicle_arrives_at=datetime.date(2021, 7, 12),\n", " vehicle_arrives_at_time=datetime.time(17),\n", " average_vehicle_capacity=90,\n", - " average_moved_capacity=90,\n", + " average_inbound_container_volume=90,\n", " next_destinations=None # Here we don't have containers that need to be grouped by destination\n", ")" ] @@ -281,13 +281,13 @@ "metadata": {}, "outputs": [], "source": [ - "port_call_manager.add_vehicle(\n", + "port_call_manager.add_service_that_calls_terminal(\n", " vehicle_type=conflowgen.ModeOfTransport.deep_sea_vessel,\n", " service_name=\"LX050\",\n", " vehicle_arrives_at=datetime.date(2021, 7, 10),\n", " vehicle_arrives_at_time=datetime.time(19),\n", " average_vehicle_capacity=16000,\n", - " average_moved_capacity=150, # for faster demo\n", + " average_inbound_container_volume=150, # to speed up the code\n", " next_destinations=[\n", " (\"ZADUR\", 0.3), # 30% of the containers go to ZADUR...\n", " (\"CNSHG\", 0.7) # and the other 70% of the containers go to CNSHG.\n", diff --git a/docs/references.bib b/docs/references.bib index c5982774..e81bd731 100644 --- a/docs/references.bib +++ b/docs/references.bib @@ -1,8 +1,8 @@ @online{destatis2021seeschifffahrt, author = {{Statistisches Bundesamt (DESTATIS)}}, title = {{Verkehr : Seeschifffahrt : August 2021}}, - howpublished = {\url{https://www.destatis.de/DE/Themen/Branchen-Unternehmen/Transport-Verkehr/Gueterverkehr/Publikationen/Downloads-Schifffahrt/seeschifffahrt-monat-2080500211084.pdf?__blob=publicationFile}}, - note = {Accessed: 2022-01-22}, + howpublished = {\url{https://www.statistischebibliothek.de/mir/receive/DEHeft_mods_00137854}}, + note = {Accessed: 2024-08-14}, year = 2021 } @@ -50,15 +50,17 @@ @online{macgregor2016containersecuring @inproceedings{kastner2022conflowgen, author = {Kastner, Marvin and Grasse, Ole and Jahn, Carlos}, + editors = {Freitag, Michael and Kinra, Aseem, and Kotzab, Herbert, and Megow, Nicole}, + booktitle = {Dynamics in Logistics. Proceedings of the 8th International Conference {LDIC} 2022, {Bremen, Germany}}, + doi = {10.1007/978-3-031-05359-7_11}, + month = {2}, + pages = {133--143}, + publisher = {Springer International Publishing}, + address = {Cham, CH}, + series = {Lecture Notes in Logistics}, title = {Container Flow Generation for Maritime Container Terminals}, - pages = {133 -- 143}, - booktitle = {Dynamics in Logistics. Proceedings of the 8th International Conference LDIC 2022, Bremen, Germany}, - eventdate = {2022-02-23/2022-02-25}, - year = {2022}, - editor = {Freitag, Michael and Kinra, Aseem and Kotzab, Herbert and Megow, Nicole}, - publisher = {Springer}, - address = {Cham, Switzerland}, - doi = {https://doi.org/10.1007/978-3-031-05359-7_11}, + year = 2022, + eventdate = {2022-02-23/2022-02-25} } @article{exposito2012marshalling, @@ -85,7 +87,7 @@ @Article{briskorn2019generator title={A generator for test instances of scheduling problems concerning cranes in transshipment terminals}, journal={OR Spectrum}, year={2019}, - month={Mar}, + month=3, day={01}, volume={41}, number={1}, @@ -115,7 +117,6 @@ @online{meisel2011unified-software } @inproceedings{edes2024estimating, - abstract = {Vessel delays and increased terminal call sizes negatively impact the ability to properly plan daily operations at seaport container terminals. Such traffic patterns lead to, among others, infrequent peak loads at the seaside of container terminals, complicating terminal operations. Thus, relying on annual or monthly statistics fails to account for these day-to-day fluctuations. When container terminals are planned, be it a greenfield or brownfield terminal, these variations in operations need to be accounted for. The traditional formula-based approach to design terminals uses annual statistics. In this study, it is first used to produce estimates for the required yard capacity for three existing exemplary container terminals. These are then compared to the results of numerical experiments using the synthetic container flow generator ConFlowGen. The findings reveal that yard capacity requirements fluctuate considerably depending on the timing of vessel arrivals and their call sizes. This dynamic modeling proved particularly beneficial for planning gateway traffic, offering more accurate storage capacity predictions. Suggestions are made for how to further develop ConFlowGen for handling transshipment traffic better in future versions.}, author = {Édes, Luc and Kastner, Marvin and Jahn, Carlos}, title = {On Estimating the Required Yard Capacity for Container Terminals}, url = {https://link.springer.com/chapter/10.1007/978-3-031-56826-8_13}, @@ -126,16 +127,16 @@ @inproceedings{edes2024estimating booktitle = {Dynamics in Logistics}, booksubtitle = {Proceedings of the 9th International Conference LDIC 2024, Bremen, Germany}, year = {2024}, - address = {Cham, DE}, + address = {Cham, CH}, doi = {10.1007/978-3-031-56826-8_13}, } @incollection{kastner2023synthetically, - abstract = {More than 80 % of world trade is delivered via sea, making the maritime supply chain a very important backbone for the economy (UNCTAD 2020). Containerized trade regularly outperforms other types of transport in terms of growth, coinciding with consistent increases of average container vessel sizes (UNCTAD 2020). Container terminal operations are heavily affected by this development, since less but larger port calls create unwanted peaks and stress on the terminals and the hinterland. Not all container terminals are affected equally by the described situation. Economic cycles and events such as the global COVID-19 pandemic or the Russian war in Ukraine change the global supply chains, trade characteristics and transport demands between ports in the world. In 2004, Hartmann proposed an approach to create scenarios for simulation and optimization in the sense of container terminal planning and logistics. Due to the significant changes in maritime trade over the years, a new approach for generating synthetic container flow data became practical. In 2021, we introduced a rethought and reworked approach on this topic.The proposed tool, named ConFlowGen, aims to assist planners, scientists, and other maritime experts with providing comprehensive container flow scenarios based on minimal inputs and assumptions of the user. In this paper, we introduce ConFlowGen's general principle of operation in an exemplary use case in the context of container terminal planning.}, author = {Kastner, Marvin and Grasse, Ole}, title = {{Synthetically generating traffic scenarios for simulation-based container terminal planning}}, - booktitle = {{PIANC Yearbook 2023 [Preprint]}}, - year = {2023}, + booktitle = {{PIANC Yearbook 2023}}, + year = {2024}, + pages = {3--17}, doi = {10.15480/882.5156}, - url = {https://tore.tuhh.de/entities/publication/c7ae66d0-4165-44e9-8481-7057af2bc775} + url = {https://pianc.app.box.com/s/vhe61hcxttpakyihjda76pk552itajmc} } diff --git a/examples/Python_Script/demo_DEHAM_CTA.py b/examples/Python_Script/demo_DEHAM_CTA.py index 65b15e3a..922db618 100644 --- a/examples/Python_Script/demo_DEHAM_CTA.py +++ b/examples/Python_Script/demo_DEHAM_CTA.py @@ -24,6 +24,7 @@ import os.path import random import sys +import subprocess import pandas as pd try: @@ -120,6 +121,7 @@ conflowgen.ContainerLength.other: 0 }) + # Add vehicles that frequently visit the terminal. port_call_manager = conflowgen.PortCallManager() @@ -165,9 +167,8 @@ service_name=feeder_vehicle_name, vehicle_arrives_at=vessel_arrives_at_as_datetime_type.date(), vehicle_arrives_at_time=vessel_arrives_at_as_datetime_type.time(), - average_vehicle_capacity=capacity, - average_moved_capacity=moved_capacity, - vehicle_arrives_every_k_days=-1, # single arrival, no frequent schedule + vehicle_capacity=capacity, + inbound_container_volume=moved_capacity, next_destinations=next_ports ) logger.info("Feeder vessels are imported") @@ -215,9 +216,8 @@ service_name=deep_sea_vessel_vehicle_name, vehicle_arrives_at=vessel_arrives_at_as_datetime_type.date(), vehicle_arrives_at_time=vessel_arrives_at_as_datetime_type.time(), - average_vehicle_capacity=capacity, - average_moved_capacity=moved_capacity, - vehicle_arrives_every_k_days=-1, # single arrival, no frequent schedule + vehicle_capacity=capacity, + inbound_container_volume=moved_capacity, next_destinations=next_ports ) logger.info("Deep sea vessels are imported") @@ -249,9 +249,8 @@ service_name=barge_vehicle_name, vehicle_arrives_at=vessel_arrives_at_as_datetime_type.date(), vehicle_arrives_at_time=vessel_arrives_at_as_datetime_type.time(), - average_vehicle_capacity=capacity, - average_moved_capacity=moved_capacity, - vehicle_arrives_every_k_days=-1 # single arrival, no frequent schedule + vehicle_capacity=capacity, + inbound_container_volume=moved_capacity, ) logger.info("Barges are imported") @@ -276,14 +275,13 @@ vehicle_arrives_at_time_as_delta = earliest_time_as_delta + datetime.timedelta(hours=0.5 * drawn_slot) vehicle_arrives_at_time = (datetime.datetime.min + vehicle_arrives_at_time_as_delta).time() logger.info(f"Add train '{train_vehicle_name}' to database") - port_call_manager.add_vehicle( + port_call_manager.add_service_that_calls_terminal( vehicle_type=conflowgen.ModeOfTransport.train, service_name=train_vehicle_name, vehicle_arrives_at=vessel_arrives_at_as_datetime_type.date(), vehicle_arrives_at_time=vehicle_arrives_at_time, average_vehicle_capacity=capacity, - average_moved_capacity=capacity, # discharge everything - vehicle_arrives_every_k_days=7 # weekly arrival + average_inbound_container_volume=capacity, ) logger.info("All trains are imported") @@ -324,4 +322,10 @@ # Gracefully close everything database_chooser.close_current_connection() +logger.info(f"ConFlowGen {conflowgen.__version__} from {conflowgen.__file__} was used.") +try: + last_git_commit = str(subprocess.check_output(["git", "log", "-1"]).strip()) # nosec B607 + logger.info("Used git commit: " + last_git_commit[2:-1]) +except: # pylint: disable=bare-except + logger.debug("The last git commit of this repository could not be retrieved, no further version specification.") logger.info("Demo 'demo_DEHAM_CTA' finished successfully.") diff --git a/examples/Python_Script/demo_DEHAM_CTA__with_ramp_up_and_ramp_down_period.py b/examples/Python_Script/demo_DEHAM_CTA__with_ramp_up_and_ramp_down_period.py new file mode 100644 index 00000000..8292bde4 --- /dev/null +++ b/examples/Python_Script/demo_DEHAM_CTA__with_ramp_up_and_ramp_down_period.py @@ -0,0 +1,335 @@ +""" +Demo for DEHAM CTA with ramp-up and ramp-down period +==================================================== + +This is a demo based on some publicly available figures, some educated guesses, and some random assumptions due to the +lack of information regarding the Container Terminal Altenwerder (CTA) in the port of Hamburg. While this demo only +poorly reflects processes in place, in addition this is only a (poor) snapshot of what has been happening in July +2021. + +No affiliations with the container terminal operators exist. Then why this example was chosen? ConFlowGen is an +extension of the work of Sönke Hartmann [1] and he presented some sample figures. For showing the similarities and +differences, similar assumptions have been made throughout this demo. + +The intention of this script is to provide a demonstration of how ConFlowGen is supposed to be used as a library. +It is, by design, a stateful library that persists all input in an SQL database format to enable reproducibility. +The intention of this demo is further explained in the logs it generated. + + +[1] Hartmann, S.: Generating scenarios for simulation and optimization of container terminal logistics. OR Spectrum, +vol. 26, 171–192 (2004). doi: 10.1007/s00291-003-0150-6 +""" + +import datetime +import os.path +import random +import sys +import subprocess +import pandas as pd + +try: + import conflowgen + install_dir = os.path.abspath( + os.path.join(conflowgen.__file__, os.path.pardir) + ) + print(f"Importing ConFlowGen version {conflowgen.__version__} installed at {install_dir}.") +except ImportError as exc: + print("Please first install ConFlowGen, e.g. with conda or pip") + raise exc + +# The seed of x=1 guarantees that the same traffic data is generated as input data in this script. However, it does not +# affect the container generation or the assignment of containers to vehicles. +seeded_random = random.Random(x=1) + +with_visuals = False +if len(sys.argv) > 2: + if sys.argv[2] == "--with-visuals": + with_visuals = True + +this_dir = os.path.dirname(__file__) + +import_deham_dir = os.path.join( + this_dir, + "data", + "DEHAM", + "CT Altenwerder" +) +df_deep_sea_vessels = pd.read_csv( + os.path.join( + import_deham_dir, + "deep_sea_vessel_input.csv" + ), + index_col=[0] +) +df_feeders = pd.read_csv( + os.path.join( + import_deham_dir, + "feeder_input.csv" + ), + index_col=[0] +) +df_barges = pd.read_csv( + os.path.join( + import_deham_dir, + "barge_input.csv" + ), + index_col=[0] +) +df_trains = pd.read_csv( + os.path.join( + import_deham_dir, + "train_input.csv" + ), + index_col=[0] +) + +# Start logging +logger = conflowgen.setup_logger() +logger.info(__doc__) + +# Pick database +database_chooser = conflowgen.DatabaseChooser( + sqlite_databases_directory=os.path.join(this_dir, "databases") +) +demo_file_name = "demo_deham_cta__with_ramp_up_and_ramp_down_period.sqlite" +database_chooser.create_new_sqlite_database( + demo_file_name, + assume_tas=True, + overwrite=True +) + +# Set settings +container_flow_generation_manager = conflowgen.ContainerFlowGenerationManager() +# Data is available for 01.07.2021 to 31.07.2021 - you can also pick a shorter time period. However, there are some +# artifacts in the generated data in the beginning and in the end of the time range because containers can not continue +# their journey as intended, i.e. they must be delivered by or picked up by a truck since no vehicles that move +# according to a schedule are generated before the start and after the end. +container_flow_start_date = datetime.date(year=2021, month=7, day=1) +container_flow_end_date = datetime.date(year=2021, month=7, day=31) +container_flow_generation_manager.set_properties( + name="Demo DEHAM CTA", + start_date=container_flow_start_date, + end_date=container_flow_end_date, + ramp_up_period=datetime.timedelta(days=10), + ramp_down_period=datetime.timedelta(days=10), +) + +# Set some general assumptions regarding the container properties +container_length_distribution_manager = conflowgen.ContainerLengthDistributionManager() +container_length_distribution_manager.set_container_length_distribution({ + conflowgen.ContainerLength.twenty_feet: 0.33, + conflowgen.ContainerLength.forty_feet: 0.67, + conflowgen.ContainerLength.forty_five_feet: 0, + conflowgen.ContainerLength.other: 0 +}) + +# Add vehicles that frequently visit the terminal. +port_call_manager = conflowgen.PortCallManager() + +logger.info("Start importing feeder vessels...") +for i, row in df_feeders.iterrows(): + feeder_vehicle_name = row["vehicle_name"] + capacity = row["capacity"] + vessel_arrives_at_as_pandas_type = row["arrival (planned)"] + vessel_arrives_at_as_datetime_type = pd.to_datetime(vessel_arrives_at_as_pandas_type) + + if vessel_arrives_at_as_datetime_type.date() < container_flow_start_date: + logger.info(f"Skipping feeder '{feeder_vehicle_name}' because it arrives before the start") + continue + + if vessel_arrives_at_as_datetime_type.date() > container_flow_end_date: + logger.info(f"Skipping feeder '{feeder_vehicle_name}' because it arrives after the end") + continue + + if port_call_manager.has_schedule(feeder_vehicle_name, vehicle_type=conflowgen.ModeOfTransport.feeder): + logger.info(f"Skipping feeder '{feeder_vehicle_name}' because it already exists") + continue + + logger.info(f"Add feeder '{feeder_vehicle_name}' to database") + # The estimate is based on the reported lifts per call in "Tendency toward Mega Containerships and the Constraints + # of Container Terminals" of Park and Suh (2019) J. Mar. Sci. Eng, URL: https://www.mdpi.com/2077-1312/7/5/131/htm + # lifts per call refer to both the inbound and outbound journey of the vessel + moved_capacity = int(round(capacity * seeded_random.triangular(0.3, 0.8) / 2)) # this is only the inbound journey + + # The actual name of the port is not important as it is only used to group containers that are destined for the same + # vessel in the yard for faster loading (less reshuffling). The assumption that the same amount of containers is + # destined for each of the following ports is a simplification. + number_ports = int(round(seeded_random.triangular(low=2, high=4))) + next_ports = [ + (port_name, 1/number_ports) + for port_name in [ + f"port {i + 1} of {feeder_vehicle_name}" + for i in range(number_ports) + ] + ] + + port_call_manager.add_vehicle( + vehicle_type=conflowgen.ModeOfTransport.feeder, + service_name=feeder_vehicle_name, + vehicle_arrives_at=vessel_arrives_at_as_datetime_type.date(), + vehicle_arrives_at_time=vessel_arrives_at_as_datetime_type.time(), + vehicle_capacity=capacity, + inbound_container_volume=moved_capacity, + next_destinations=next_ports + ) +logger.info("Feeder vessels are imported") + +logger.info("Start importing deep sea vessels...") +for i, row in df_deep_sea_vessels.iterrows(): + deep_sea_vessel_vehicle_name = row["vehicle_name"] + capacity = row["capacity"] + vessel_arrives_at_as_pandas_type = row["arrival (planned)"] + vessel_arrives_at_as_datetime_type = pd.to_datetime(vessel_arrives_at_as_pandas_type) + + if vessel_arrives_at_as_datetime_type.date() < container_flow_start_date: + logger.info(f"Skipping deep sea vessel '{deep_sea_vessel_vehicle_name}' because it arrives before the start") + continue + + if vessel_arrives_at_as_datetime_type.date() > container_flow_end_date: + logger.info(f"Skipping deep sea vessel '{deep_sea_vessel_vehicle_name}' because it arrives after the end") + continue + + if port_call_manager.has_schedule(deep_sea_vessel_vehicle_name, + vehicle_type=conflowgen.ModeOfTransport.deep_sea_vessel): + logger.info(f"Skipping deep sea service '{deep_sea_vessel_vehicle_name}' because it already exists") + continue + + logger.info(f"Add deep sea vessel '{deep_sea_vessel_vehicle_name}' to database") + # The estimate is based on the reported lifts per call in "Tendency toward Mega Containerships and the Constraints + # of Container Terminals" of Park and Suh (2019) J. Mar. Sci. Eng, URL: https://www.mdpi.com/2077-1312/7/5/131/htm + # lifts per call refer to both the inbound and outbound journey of the vessel + moved_capacity = int(round(capacity * seeded_random.triangular(0.3, 0.6) / 2)) # this is only the inbound journey + + # The actual name of the port is not important as it is only used to group containers that are destined for the same + # vessel in the yard for faster loading (less reshuffling). The assumption that the same amount of containers is + # destined for each of the following ports is a simplification + number_ports = int(round(seeded_random.triangular(low=3, high=15, mode=7))) + next_ports = [ + (port_name, 1/number_ports) + for port_name in [ + f"port {i + 1} of {deep_sea_vessel_vehicle_name}" + for i in range(number_ports) + ] + ] + + port_call_manager.add_vehicle( + vehicle_type=conflowgen.ModeOfTransport.deep_sea_vessel, + service_name=deep_sea_vessel_vehicle_name, + vehicle_arrives_at=vessel_arrives_at_as_datetime_type.date(), + vehicle_arrives_at_time=vessel_arrives_at_as_datetime_type.time(), + vehicle_capacity=capacity, + inbound_container_volume=moved_capacity, + next_destinations=next_ports + ) +logger.info("Deep sea vessels are imported") + +logger.info("Start importing barges...") +for i, row in df_barges.iterrows(): + barge_vehicle_name = row["vehicle_name"] + capacity = row["capacity"] + vessel_arrives_at_as_pandas_type = row["arrival (planned)"] + vessel_arrives_at_as_datetime_type = pd.to_datetime(vessel_arrives_at_as_pandas_type) + + if vessel_arrives_at_as_datetime_type.date() < container_flow_start_date: + logger.info(f"Skipping barge '{barge_vehicle_name}' because it arrives before the start") + continue + + if vessel_arrives_at_as_datetime_type.date() > container_flow_end_date: + logger.info(f"Skipping barge '{barge_vehicle_name}' because it arrives after the end") + continue + + if port_call_manager.has_schedule(barge_vehicle_name, vehicle_type=conflowgen.ModeOfTransport.barge): + logger.info(f"Skipping barge '{barge_vehicle_name}' because it already exists") + continue + + logger.info(f"Add barge '{barge_vehicle_name}' to database") + # assume that the barge approaches 2 or more terminals, thus not the whole barge is available for CTA + moved_capacity = int(round(capacity * seeded_random.uniform(0.3, 0.6))) + port_call_manager.add_vehicle( + vehicle_type=conflowgen.ModeOfTransport.barge, + service_name=barge_vehicle_name, + vehicle_arrives_at=vessel_arrives_at_as_datetime_type.date(), + vehicle_arrives_at_time=vessel_arrives_at_as_datetime_type.time(), + vehicle_capacity=capacity, + inbound_container_volume=moved_capacity, + ) +logger.info("Barges are imported") + +logger.info("Start importing trains...") +for i, row in df_trains.iterrows(): + train_vehicle_name = row["vehicle_name"] + vessel_arrives_at_as_pandas_type = row["arrival_day"] + vessel_arrives_at_as_datetime_type = pd.to_datetime(vessel_arrives_at_as_pandas_type) + + if port_call_manager.has_schedule(train_vehicle_name, vehicle_type=conflowgen.ModeOfTransport.train): + logger.info(f"Train service '{train_vehicle_name}' already exists") + continue + + capacity = 96 # in TEU, see https://www.intermodal-info.com/verkehrstraeger/ + earliest_time = datetime.time(hour=1, minute=0) + earliest_time_as_delta = datetime.timedelta(hours=earliest_time.hour, minutes=earliest_time.minute) + latest_time = datetime.time(hour=5, minute=30) + latest_time_as_delta = datetime.timedelta(hours=latest_time.hour, minutes=latest_time.minute) + number_slots_minus_one = int((latest_time_as_delta - earliest_time_as_delta) / datetime.timedelta(minutes=30)) + + drawn_slot = seeded_random.randint(0, number_slots_minus_one) + vehicle_arrives_at_time_as_delta = earliest_time_as_delta + datetime.timedelta(hours=0.5 * drawn_slot) + vehicle_arrives_at_time = (datetime.datetime.min + vehicle_arrives_at_time_as_delta).time() + logger.info(f"Add train '{train_vehicle_name}' to database") + port_call_manager.add_service_that_calls_terminal( + vehicle_type=conflowgen.ModeOfTransport.train, + service_name=train_vehicle_name, + vehicle_arrives_at=vessel_arrives_at_as_datetime_type.date(), + vehicle_arrives_at_time=vehicle_arrives_at_time, + average_vehicle_capacity=capacity, + average_inbound_container_volume=capacity, + ) + +logger.info("All trains are imported") + +logger.info("All vehicles have been imported") + +### +# Now, all schedules and input distributions are set up - no further inputs are required +### + +logger.info("Preview the results with some light-weight approaches.") + +conflowgen.run_all_previews( + as_graph=with_visuals +) + +logger.info("Generate all fleets with all vehicles. This is the core of the whole project.") +container_flow_generation_manager.generate() + +logger.info("The container flow data have been generated, run analyses on them.") + +conflowgen.run_all_analyses( + as_graph=with_visuals +) + +logger.info("For a better understanding of the data, it is advised to study the logs and compare the preview with the " + "analysis results.") + +logger.info("Start data export...") + +# Export important entries from SQL to CSV so that it can be further processed, e.g., by a simulation software +export_container_flow_manager = conflowgen.ExportContainerFlowManager() +export_container_flow_manager.export( + folder_name=( + "demo-DEHAM-inc-ramp-up-and-down-of-day--" + + str(datetime.datetime.now()).replace(":", "-").replace(" ", "--").split(".")[0] + ), + path_to_export_folder=os.path.join(this_dir, "export"), + file_format=conflowgen.ExportFileFormat.csv +) + +# Gracefully close everything +database_chooser.close_current_connection() +logger.info(f"ConFlowGen {conflowgen.__version__} from {conflowgen.__file__} was used.") +try: + last_git_commit = str(subprocess.check_output(["git", "log", "-1"]).strip()) # nosec B607 + logger.info("Used git commit: " + last_git_commit[2:-1]) +except: # pylint: disable=bare-except + logger.debug("The last git commit of this repository could not be retrieved, no further version specification.") +logger.info("Demo 'demo_DEHAM_CTA_with_ramp_up_and_down_period' finished successfully.") diff --git a/examples/Python_Script/demo_continental_gateway.py b/examples/Python_Script/demo_continental_gateway.py index 4e9de1e5..0560470a 100644 --- a/examples/Python_Script/demo_continental_gateway.py +++ b/examples/Python_Script/demo_continental_gateway.py @@ -7,6 +7,7 @@ import os.path import random import sys +import subprocess import pandas as pd try: @@ -168,9 +169,8 @@ service_name=feeder_vehicle_name, vehicle_arrives_at=vessel_arrives_at_as_datetime_type.date(), vehicle_arrives_at_time=vessel_arrives_at_as_datetime_type.time(), - average_vehicle_capacity=capacity, - average_moved_capacity=moved_capacity, - vehicle_arrives_every_k_days=-1, # single arrival, no frequent schedule + vehicle_capacity=capacity, + inbound_container_volume=moved_capacity, next_destinations=next_ports ) logger.info("Feeder vessels are imported") @@ -196,4 +196,10 @@ # Gracefully close everything database_chooser.close_current_connection() +logger.info(f"ConFlowGen {conflowgen.__version__} from {conflowgen.__file__} was used.") +try: + last_git_commit = str(subprocess.check_output(["git", "log", "-1"]).strip()) # nosec B607 + logger.info("Used git commit: " + last_git_commit[2:-1]) +except: # pylint: disable=bare-except + logger.debug("The last git commit of this repository could not be retrieved, no further version specification.") logger.info("Demo 'demo_continental_gateway' finished successfully.") diff --git a/examples/Python_Script/demo_poc.py b/examples/Python_Script/demo_poc.py index fb0a1844..10aa4d45 100644 --- a/examples/Python_Script/demo_poc.py +++ b/examples/Python_Script/demo_poc.py @@ -12,8 +12,9 @@ """ import datetime -import os +import os.path import sys +import subprocess try: import conflowgen @@ -25,6 +26,7 @@ print("Please first install ConFlowGen, e.g. with conda or pip") raise exc + this_dir = os.path.dirname(__file__) with_visuals = False @@ -61,13 +63,13 @@ # Add vehicles that frequently visit the terminal. feeder_service_name = "LX050" logger.info(f"Add feeder service '{feeder_service_name}' to database") -port_call_manager.add_vehicle( +port_call_manager.add_service_that_calls_terminal( vehicle_type=conflowgen.ModeOfTransport.feeder, service_name=feeder_service_name, vehicle_arrives_at=datetime.date(2021, 7, 9), vehicle_arrives_at_time=datetime.time(11), average_vehicle_capacity=800, - average_moved_capacity=100, + average_inbound_container_volume=100, next_destinations=[ ("DEBRV", 0.4), # 50% of the containers (in boxes) go here... ("RULED", 0.6) # and the other 50% of the containers (in boxes) go here. @@ -76,25 +78,25 @@ train_service_name = "JR03A" logger.info(f"Add train service '{train_service_name}' to database") -port_call_manager.add_vehicle( +port_call_manager.add_service_that_calls_terminal( vehicle_type=conflowgen.ModeOfTransport.train, service_name=train_service_name, vehicle_arrives_at=datetime.date(2021, 7, 12), vehicle_arrives_at_time=datetime.time(17), average_vehicle_capacity=90, - average_moved_capacity=90, + average_inbound_container_volume=90, next_destinations=None # Here we don't have containers that need to be grouped by destination ) deep_sea_service_name = "LX050" logger.info(f"Add deep sea vessel service '{deep_sea_service_name}' to database") -port_call_manager.add_vehicle( +port_call_manager.add_service_that_calls_terminal( vehicle_type=conflowgen.ModeOfTransport.deep_sea_vessel, service_name=deep_sea_service_name, vehicle_arrives_at=datetime.date(2021, 7, 10), vehicle_arrives_at_time=datetime.time(19), average_vehicle_capacity=16000, - average_moved_capacity=150, # for faster demo + average_inbound_container_volume=150, # for faster demo next_destinations=[ ("ZADUR", 0.3), # 30% of the containers (in boxes) go here... ("CNSHG", 0.7) # and the other 70% of the containers (in boxes) go here. @@ -136,4 +138,10 @@ # Gracefully close everything database_chooser.close_current_connection() +logger.info(f"ConFlowGen {conflowgen.__version__} from {conflowgen.__file__} was used.") +try: + last_git_commit = str(subprocess.check_output(["git", "log", "-1"]).strip()) # nosec B607 + logger.info("Used git commit: " + last_git_commit[2:-1]) +except: # pylint: disable=bare-except + logger.debug("The last git commit of this repository could not be retrieved, skip this.") logger.info("Demo 'demo_poc' finished successfully.") diff --git a/run_ci_light.bat b/run_ci_light.bat index c06d2132..ed8e1594 100644 --- a/run_ci_light.bat +++ b/run_ci_light.bat @@ -49,6 +49,7 @@ python -m pip install -e .[dev] || ( ) REM run tests +set MPLBACKEND=agg python -m pytest --exitfirst --verbose --failed-first --cov="./conflowgen" --cov-report html || ( ECHO.Tests failed! EXIT /B diff --git a/setup.py b/setup.py index 245d6576..9abae58b 100644 --- a/setup.py +++ b/setup.py @@ -50,6 +50,7 @@ 'pytest-cov', # create coverage report 'pytest-xdist', # use several processes to speed up the testing process 'pytest-github-actions-annotate-failures', # turns pytest failures into action annotations + 'parameterized', # for parameterized testing 'seaborn', # some visuals in unittests are generated by seaborn 'nbconvert', # used to run tests in Jupyter notebooks, see ./test/notebooks/test_run_notebooks.py