Skip to content

Commit

Permalink
Merge branch 'pr/209' of github.com:1kastner/conflowgen into pr/209
Browse files Browse the repository at this point in the history
  • Loading branch information
1kastner committed May 30, 2024
2 parents 2f2bb00 + ef3a103 commit d2f0401
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 49 deletions.
14 changes: 9 additions & 5 deletions conflowgen/analyses/abstract_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions conflowgen/analyses/container_dwell_time_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down
36 changes: 27 additions & 9 deletions conflowgen/analyses/container_flow_by_vehicle_instance_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
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
Expand All @@ -17,27 +19,34 @@ class ContainerFlowByVehicleInstanceAnalysis(AbstractAnalysis):
as it is the case with :class:`.ContainerFlowByVehicleInstanceAnalysisReport`.
"""

@staticmethod
@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
) -> typing.Dict[
end_date: typing.Optional[datetime.datetime] = None,
) -> [
ModeOfTransport, typing.Dict[VehicleIdentifier, typing.Dict[FlowDirection, (int, int)]]
]:
"""
This shows for each of the vehicles
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 much import, export, and transshipment is unloaded and
loaded.
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]]]] = {
Expand All @@ -47,8 +56,12 @@ def get_container_flow_by_vehicle(

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 Container.select():
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:
Expand Down Expand Up @@ -77,7 +90,8 @@ def get_container_flow_by_vehicle(
for flow_direction in FlowDirection
}
container_flow_by_vehicle[
container.delivered_by][vehicle_id_inbound][container.flow_direction]["inbound"] += 1
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

Expand All @@ -104,4 +118,8 @@ def get_container_flow_by_vehicle(
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

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.descriptive_datatypes import VehicleIdentifier, FlowDirection
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
Expand All @@ -35,10 +35,15 @@ def __init__(self):
self.analysis = ContainerFlowByVehicleInstanceAnalysis()

def get_report_as_text(
self, **kwargs
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.
Expand All @@ -47,38 +52,39 @@ def get_report_as_text(
Returns:
The report in human-readable text format
"""
container_flow = self._get_analysis(kwargs)
plain_table, vehicle_types = self._get_analysis(kwargs)

report = "(pending)"
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)"

# create string representation
return report

def _get_analysis(self, kwargs: dict) -> typing.Dict[
ModeOfTransport, typing.Dict[VehicleIdentifier, typing.Dict[FlowDirection, (int, int)]]
]:
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
end_date=end_date,
)
return container_flow

def get_report_as_graph(self, **kwargs) -> matplotlib.figure.Figure:
"""
Visualize the container flows (import, export, transshipment) over time.

Returns:
The diagram.
"""
container_flow = self._get_analysis(kwargs)
vehicle_types = list(container_flow.keys())

plain_table = []

for mode_of_transport in (set(container_flow.keys()) - set([ModeOfTransport.truck])):
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]:
Expand All @@ -91,35 +97,59 @@ def get_report_as_graph(self, **kwargs) -> matplotlib.figure.Figure:
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

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)

self._df = df
df = self._get_dataframe_from_plain_table(plain_table)

fig, axes = plt.subplots(nrows=2 * (len(ModeOfTransport) - 1), figsize=(7, 20))
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 (set(ModeOfTransport) - set([ModeOfTransport.truck])):
for mode_of_transport in vehicle_types:
for journey_direction in ["inbound", "outbound"]:
ax = axes[i]
i += 1
ax.set_title(f"{mode_of_transport} - {journey_direction}")
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down
4 changes: 2 additions & 2 deletions conflowgen/analyses/modal_split_analysis_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit d2f0401

Please sign in to comment.