Skip to content

Commit

Permalink
Add quayside throughput preview
Browse files Browse the repository at this point in the history
  • Loading branch information
1kastner committed Sep 9, 2023
1 parent 2792c2f commit 31fef5a
Show file tree
Hide file tree
Showing 13 changed files with 529 additions and 195 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def get_truck_capacity_for_export_containers(
Thus, this method accounts for both import and export.
"""
truck_capacity = 0
vehicle_type: ModeOfTransport
for vehicle_type in ModeOfTransport.get_scheduled_vehicles():
number_of_containers_delivered_to_terminal_by_vehicle_type = inbound_capacity_of_vehicles[vehicle_type]
mode_of_transport_distribution_of_vehicle_type = \
Expand All @@ -50,40 +51,44 @@ def get_inbound_capacity_of_vehicles(
depending on the outbound distribution, are created based on the assumptions of the further container flow
generation process.
"""
containers: Dict[ModeOfTransport, float] = {
inbound_container_volume_in_containers: Dict[ModeOfTransport, float] = {
vehicle_type: 0
for vehicle_type in ModeOfTransport
}
inbound_capacity_in_teu: Dict[ModeOfTransport, float] = {
inbound_container_volume_in_teu: Dict[ModeOfTransport, float] = {
vehicle_type: 0
for vehicle_type in ModeOfTransport
}

at_least_one_schedule_exists: bool = False

for schedule in Schedule.select():
at_least_one_schedule_exists = True
arrivals = create_arrivals_within_time_range(
start_date,
schedule.vehicle_arrives_at,
end_date,
schedule.vehicle_arrives_every_k_days,
schedule.vehicle_arrives_at_time
)
total_capacity_moved_by_vessel = (len(arrivals) # number of vehicles that are planned
* schedule.average_moved_capacity) # TEU capacity of each vehicle
containers[schedule.vehicle_type] += total_capacity_moved_by_vessel / \
(ContainerLengthDistributionRepository.get_teu_factor() * 20)
inbound_capacity_in_teu[schedule.vehicle_type] += total_capacity_moved_by_vessel

inbound_capacity_in_teu[ModeOfTransport.truck] = \
InboundAndOutboundVehicleCapacityCalculatorService.get_truck_capacity_for_export_containers(
inbound_capacity_in_teu
)
containers[ModeOfTransport.truck] = \
inbound_capacity_in_teu[ModeOfTransport.truck] / \
(ContainerLengthDistributionRepository.get_teu_factor() * 20)
moved_inbound_volumes = (len(arrivals) # number of vehicles that are planned
* schedule.average_moved_capacity) # 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()

if at_least_one_schedule_exists:
inbound_container_volume_in_teu[ModeOfTransport.truck] = \
InboundAndOutboundVehicleCapacityCalculatorService.get_truck_capacity_for_export_containers(
inbound_container_volume_in_teu
)
inbound_container_volume_in_containers[ModeOfTransport.truck] = \
inbound_container_volume_in_teu[ModeOfTransport.truck] / \
ContainerLengthDistributionRepository.get_teu_factor()

return ContainerVolumeByVehicleType(
containers=containers,
teu=inbound_capacity_in_teu
containers=inbound_container_volume_in_containers,
teu=inbound_container_volume_in_teu
)

@staticmethod
Expand Down Expand Up @@ -130,10 +135,10 @@ 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
total_average_capacity_moved_by_vessel_in_teu = len(arrivals) * schedule.average_moved_capacity
outbound_used_capacity_in_teu[schedule.vehicle_type] += total_average_capacity_moved_by_vessel_in_teu
outbound_used_containers[schedule.vehicle_type] += total_average_capacity_moved_by_vessel_in_teu / \
(ContainerLengthDistributionRepository.get_teu_factor() * 20)
container_volume_moved_by_vessels_in_teu = len(arrivals) * schedule.average_moved_capacity
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(
Expand All @@ -143,7 +148,7 @@ def get_outbound_capacity_of_vehicles(start_date, end_date, transportation_buffe
total_maximum_capacity_moved_by_vessel = len(arrivals) * maximum_capacity_of_vehicle_in_teu
outbound_maximum_capacity_in_teu[schedule.vehicle_type] += total_maximum_capacity_moved_by_vessel
outbound_maximum_containers[schedule.vehicle_type] += total_maximum_capacity_moved_by_vessel / \
(ContainerLengthDistributionRepository.get_teu_factor() * 20)
ContainerLengthDistributionRepository.get_teu_factor()

inbound_capacity = InboundAndOutboundVehicleCapacityCalculatorService.\
get_inbound_capacity_of_vehicles(start_date, end_date)
Expand All @@ -153,7 +158,7 @@ def get_outbound_capacity_of_vehicles(start_date, end_date, transportation_buffe
)
outbound_used_containers[ModeOfTransport.truck] = \
outbound_used_capacity_in_teu[ModeOfTransport.truck] / \
(ContainerLengthDistributionRepository.get_teu_factor() * 20)
ContainerLengthDistributionRepository.get_teu_factor()

outbound_maximum_capacity_in_teu[ModeOfTransport.truck] = np.nan # Trucks can always be added as required
outbound_maximum_containers[ModeOfTransport.truck] = np.nan
Expand Down
37 changes: 30 additions & 7 deletions conflowgen/descriptive_datatypes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,26 @@ class HinterlandModalSplit(typing.NamedTuple):
truck_capacity: float


class OutboundUsedAndMaximumCapacity(typing.NamedTuple):
class ContainerVolume(typing.NamedTuple):
"""
This tuple keeps track of how much each vehicle type transports on the outbound journey and what the maximum
capacity is.
Several KPIs at container terminals can be both expressed in boxes and TEU.
"""
#: The container volume expressed in TEU
teu: float

#: The container volume that is actually transported, summarized by vehicle type.
used: ContainerVolumeByVehicleType
#: The container volume expressed in number of boxes
containers: float

#: The container volume that could be transported if all capacities had been used, summarized by vehicle type.
maximum: ContainerVolumeByVehicleType

class InboundAndOutboundContainerVolume(typing.NamedTuple):
"""
Note both the inbound and outbound container volume.
"""
#: The container volume transported by vehicles on their inbound journey
inbound: ContainerVolume

#: The container volume transported by vehicles on their outbound journey
outbound: ContainerVolume


class ContainerVolumeByVehicleType(typing.NamedTuple):
Expand All @@ -52,6 +61,20 @@ class ContainerVolumeByVehicleType(typing.NamedTuple):
containers: typing.Optional[typing.Dict[ModeOfTransport, float]]


class OutboundUsedAndMaximumCapacity(typing.NamedTuple):
"""
This tuple keeps track of how much each vehicle type transports on the outbound journey and what the maximum
capacity is.
"""

#: The container volume that is actually transported, summarized by vehicle type.
used: ContainerVolumeByVehicleType

#: The container volume that could be transported if all capacities had been used, summarized by vehicle type.
maximum: ContainerVolumeByVehicleType



class ContainerVolumeFromOriginToDestination(typing.NamedTuple):
"""
Several KPIs at container terminals can be both expressed in boxes per hour and TEU per hour (or a different time
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import math
from typing import Dict

from conflowgen.data_summaries.data_summaries_cache import DataSummariesCache
from conflowgen.domain_models.distribution_models.container_length_distribution import ContainerLengthDistribution
from conflowgen.domain_models.data_types.container_length import ContainerLength

Expand Down Expand Up @@ -55,6 +56,7 @@ def set_distribution(cls, container_lengths: Dict[ContainerLength, float]):
).save()

@classmethod
@DataSummariesCache.cache_result
def get_teu_factor(cls) -> float:
"""
Calculates and returns the TEU factor based on the container length distribution.
Expand Down
2 changes: 2 additions & 0 deletions conflowgen/previews/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .inbound_and_outbound_vehicle_capacity_preview_report import InboundAndOutboundVehicleCapacityPreviewReport
from .container_flow_by_vehicle_type_preview_report import ContainerFlowByVehicleTypePreviewReport
from .modal_split_preview_report import ModalSplitPreviewReport
from .quay_side_throughput_preview_report import QuaySideThroughputPreviewReport
from .truck_gate_throughput_preview_report import TruckGateThroughputPreviewReport
from .vehicle_capacity_exceeded_preview_report import VehicleCapacityUtilizationOnOutboundJourneyPreviewReport
from ..reporting import AbstractReport
Expand All @@ -15,6 +16,7 @@
VehicleCapacityUtilizationOnOutboundJourneyPreviewReport,
ContainerFlowByVehicleTypePreviewReport,
ModalSplitPreviewReport,
QuaySideThroughputPreviewReport,
TruckGateThroughputPreviewReport
]

Expand Down
81 changes: 81 additions & 0 deletions conflowgen/previews/quay_side_throughput_preview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import typing
from abc import ABC
from datetime import datetime

from conflowgen.data_summaries.data_summaries_cache import DataSummariesCache
from conflowgen.domain_models.distribution_repositories.container_length_distribution_repository import \
ContainerLengthDistributionRepository
from conflowgen.previews.container_flow_by_vehicle_type_preview import ContainerFlowByVehicleTypePreview
from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport
from conflowgen.domain_models.distribution_validators import validate_distribution_with_one_dependent_variable
from conflowgen.previews.abstract_preview import AbstractPreview
from conflowgen.descriptive_datatypes import InboundAndOutboundContainerVolume, ContainerVolume


class QuaySideThroughputPreview(AbstractPreview, ABC):
"""
This preview calculates the quayside throughput based on the schedules.
The preview returns a data structure that can be used for generating reports (e.g., in text or as a figure). The
preview is intended to provide an estimate of the quayside throughput for the given inputs.
"""

QUAY_SIDE_VEHICLES = {
ModeOfTransport.deep_sea_vessel,
ModeOfTransport.feeder,
# barges are counted as hinterland here
}

def __init__(self, start_date: datetime.date, end_date: datetime.date, transportation_buffer: float):
super().__init__(start_date, end_date, transportation_buffer)
self.container_flow_by_vehicle_type = (
ContainerFlowByVehicleTypePreview(
self.start_date,
self.end_date,
self.transportation_buffer,
)
)

@DataSummariesCache.cache_result
def hypothesize_with_mode_of_transport_distribution(
self,
mode_of_transport_distribution: typing.Dict[ModeOfTransport, typing.Dict[ModeOfTransport, float]]
):
validate_distribution_with_one_dependent_variable(
mode_of_transport_distribution, ModeOfTransport, ModeOfTransport, values_are_frequencies=True
)
self.container_flow_by_vehicle_type.hypothesize_with_mode_of_transport_distribution(
mode_of_transport_distribution
)


@DataSummariesCache.cache_result
def get_quay_side_throughput(self) -> InboundAndOutboundContainerVolume:
inbound_to_outbound_flow = self.container_flow_by_vehicle_type.get_inbound_to_outbound_flow()

quayside_inbound_container_volume_in_teu: int = 0
quayside_outbound_container_volume_in_teu: int = 0

inbound_vehicle_type: ModeOfTransport
outbound_vehicle_type: ModeOfTransport
for inbound_vehicle_type, to_outbound_flow in inbound_to_outbound_flow.items():
for outbound_vehicle_type, container_volume in to_outbound_flow.items():
if inbound_vehicle_type in self.QUAY_SIDE_VEHICLES:
quayside_inbound_container_volume_in_teu += container_volume
if outbound_vehicle_type in self.QUAY_SIDE_VEHICLES:
quayside_outbound_container_volume_in_teu += container_volume

teu_factor = ContainerLengthDistributionRepository().get_teu_factor()

result = InboundAndOutboundContainerVolume(
inbound=ContainerVolume(
teu=quayside_inbound_container_volume_in_teu,
containers=quayside_inbound_container_volume_in_teu / teu_factor
),
outbound=ContainerVolume(
teu=quayside_outbound_container_volume_in_teu,
containers=quayside_outbound_container_volume_in_teu / teu_factor
)
)

return result
86 changes: 86 additions & 0 deletions conflowgen/previews/quay_side_throughput_preview_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from __future__ import annotations

from typing import Dict

import pandas as pd

from conflowgen.descriptive_datatypes import InboundAndOutboundContainerVolume
from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport
from conflowgen.previews.quay_side_throughput_preview import QuaySideThroughputPreview
from conflowgen.reporting import AbstractReportWithMatplotlib


class QuaySideThroughputPreviewReport(AbstractReportWithMatplotlib):
"""
This preview report takes the data structure as generated by
:class:`.QuaySideThroughputPreview`
and creates a comprehensible representation for the user, either as text or as a graph.
"""

report_description = """
This report previews the inbound and outbound traffic at the quay side.
This is only an estimate, additional restrictions (such as the dwell time restrictions) might further
reduce the number of containers one vehicle can in fact pick up for its outbound journey.
"""

def __init__(self):
super().__init__()
self._df = None
self.preview = QuaySideThroughputPreview(
start_date=self.start_date,
end_date=self.end_date,
transportation_buffer=self.transportation_buffer
)

def hypothesize_with_mode_of_transport_distribution(
self,
mode_of_transport_distribution: Dict[ModeOfTransport, Dict[ModeOfTransport, float]]
):
self.preview.hypothesize_with_mode_of_transport_distribution(mode_of_transport_distribution)

def get_report_as_text(
self, **kwargs
) -> str:
assert len(kwargs) == 0, f"No keyword arguments supported for {self.__class__.__name__}"

quay_side_throughput = self._get_quay_side_throughput()

# create string representation
report = "\n"
report += "discharged (in containers) "
report += "loaded (in containers)"
report += "\n"

report += f"{int(round(quay_side_throughput.inbound.containers)):>26} "
report += f"{int(round(quay_side_throughput.outbound.containers)):>22}"
report += "\n"

report += "(rounding errors might exist)\n"
return report

def get_report_as_graph(self, **kwargs) -> object:
assert len(kwargs) == 0, f"No keyword arguments supported for {self.__class__.__name__}"

quay_side_throughput = self._get_quay_side_throughput()

series = pd.Series({
"Number discharged containers": quay_side_throughput.inbound.containers,
"Number loaded containers": quay_side_throughput.outbound.containers
}, name="Quayside Throughput")

ax = series.plot()

return ax

def _get_quay_side_throughput(self) -> InboundAndOutboundContainerVolume:
assert self.start_date is not None
assert self.end_date is not None
assert self.transportation_buffer is not None
self.preview.update(
start_date=self.start_date,
end_date=self.end_date,
transportation_buffer=self.transportation_buffer
)
# gather data
quay_side_throughput = self.preview.get_quay_side_throughput()
return quay_side_throughput
Loading

0 comments on commit 31fef5a

Please sign in to comment.