Skip to content

Commit

Permalink
Fix time chaching of container (#190)
Browse files Browse the repository at this point in the history
* Drop fixed Sphinx version

* explain why some variables are good names

* elaborate on excluded patterns in documentation

* drop cache_rsult from container.get_arrival_time and container.get_departure_time, add typehints for fields

* Fix pylint issues

* get_factor -> get_teu_factor
  • Loading branch information
1kastner authored Sep 9, 2023
1 parent f348838 commit a7d44f6
Show file tree
Hide file tree
Showing 16 changed files with 95 additions and 66 deletions.
27 changes: 13 additions & 14 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -185,18 +185,17 @@ function-naming-style=snake_case
#function-rgx=

# Good variable names which should always be accepted, separated by a comma.
good-names=i,
j,
k,
ex,
Run,
_,
df,
ax,
x,
xs,
mu,
gs
good-names=i, # typical counter variable
j, # typical counter variable
k, # typical counter variable
_, # typical discard result indicator
df, # pandas DataFrame
ax, # matplotlib axis
gs, # matplotlib gridspec
x, # one element on the x axis
xs, # collection of elements on the x axis
mu, # mu of, e.g., a normal distribution
sd, # standard deviation of, e.g., a normal distribution

# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
Expand Down Expand Up @@ -547,5 +546,5 @@ preferred-modules=

# Exceptions that will emit a warning when being caught. Defaults to
# "BaseException, Exception".
overgeneral-exceptions=BaseException,
Exception
overgeneral-exceptions=builtins.BaseException,
builtins.Exception
40 changes: 22 additions & 18 deletions conflowgen/domain_models/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
from peewee import ForeignKeyField
from peewee import IntegerField

from conflowgen.data_summaries.data_summaries_cache import DataSummariesCache
from .arrival_information import TruckArrivalInformationForDelivery, TruckArrivalInformationForPickup
from .base_model import BaseModel
from .data_types.container_length import CONTAINER_LENGTH_TO_OCCUPIED_TEU
from .data_types.container_length import CONTAINER_LENGTH_TO_OCCUPIED_TEU, ContainerLength
from .field_types.container_length import ContainerLengthField
from .field_types.mode_of_transport import ModeOfTransportField
from .field_types.storage_requirement import StorageRequirementField
from .large_vehicle_schedule import Destination
from .vehicle import LargeScheduledVehicle
from .vehicle import Truck
from .data_types.storage_requirement import StorageRequirement
from ..domain_models.data_types.mode_of_transport import ModeOfTransport


Expand All @@ -35,78 +35,78 @@ def __init__(self, container, vehicle_type):
class Container(BaseModel):
"""A representation of the physical container that is moved through the yard."""
id = AutoField()
weight = IntegerField(
weight: int = IntegerField(
null=False,
help_text="The weight of the container (approximated). This value should suit to the container weight "
"distribution."
)
length = ContainerLengthField(
length: ContainerLength = ContainerLengthField(
null=False,
help_text="The length of the container in feet, typically 20' or 40' are used in international trade."
)
storage_requirement = StorageRequirementField(
storage_requirement: StorageRequirement = StorageRequirementField(
null=False,
help_text="Some containers must be stored separately, e.g. if they are reefers or dangerous goods containers."
)
delivered_by = ModeOfTransportField(
delivered_by: ModeOfTransport = ModeOfTransportField(
null=False,
help_text="This vehicle type delivers this container to the terminal. This helps to quickly pick the correct "
"foreign key in the next step and is thus just additional information."
)
picked_up_by_initial = ModeOfTransportField(
picked_up_by_initial: ModeOfTransport = ModeOfTransportField(
null=False,
help_text="This vehicle type is first drawn randomly for picking up the container. It might be overwritten "
"later because no vehicle satisfies the constraints, e.g. because all vehicles of that type arrive "
"too early or too late or they are already full. Large deviations between `picked_up_by_initial` "
"and `picked_up_by` might indicate calibration issues with the random distributions or schedules."
)
picked_up_by = ModeOfTransportField(
picked_up_by: ModeOfTransport = ModeOfTransportField(
null=False,
help_text="This vehicle type is later actually used for picking up the container. This helps to quickly pick "
"the correct foreign key in the next step and is thus just additional information."
)
delivered_by_large_scheduled_vehicle = ForeignKeyField(
delivered_by_large_scheduled_vehicle: LargeScheduledVehicle = ForeignKeyField(
LargeScheduledVehicle,
null=True,
help_text="Points at the large scheduled vehicle it is delivered by (null if truck). "
"Any arrival information of the container is attached to that vehicle."
)
delivered_by_truck = ForeignKeyField(
delivered_by_truck: Truck = ForeignKeyField(
Truck,
null=True,
help_text="Points at the truck it is delivered by (null if large scheduled vehicle). "
"Any arrival information of the container is attached to that vehicle)."
)
picked_up_by_large_scheduled_vehicle = ForeignKeyField(
picked_up_by_large_scheduled_vehicle: LargeScheduledVehicle = ForeignKeyField(
LargeScheduledVehicle,
null=True,
help_text="Points at the large scheduled vehicle it is picked up by (null if truck). "
"Any departure information of the container is attached to that vehicle."
)
picked_up_by_truck = ForeignKeyField(
picked_up_by_truck: Truck = ForeignKeyField(
Truck,
null=True,
help_text="Points at the truck it is picked up by (null if large scheduled vehicle). "
"Any departure information of the container is attached to that vehicle."
)
destination = ForeignKeyField(
destination: Destination = ForeignKeyField(
Destination,
null=True,
help_text="Points at the next destination of the container. Only applicable if picked up by a large scheduled "
"vehicle. This information is sometimes used for better container stacking in the yard. For vessels, "
"this can be regarded as a simplified stowage plan, likewise for trains and barges."
)
emergency_pickup = BooleanField(
emergency_pickup: bool = BooleanField(
default=False,
help_text="This indicates that no regular means of transport was available so that a vehicle had to be called "
"explicitly to pick up the container so that the maximum dwell time is not exceeded."
)
cached_arrival_time = DateTimeField(
cached_arrival_time: datetime.datetime = DateTimeField(
default=None,
null=True,
help_text="This field is used to cache the arrival time for faster evaluation of analyses."
)
cached_departure_time = DateTimeField(
cached_departure_time: datetime.datetime = DateTimeField(
default=None,
null=True,
help_text="This field is used to cache the departure time for faster evaluation of analyses."
Expand All @@ -116,9 +116,11 @@ class Container(BaseModel):
def occupied_teu(self) -> float:
return CONTAINER_LENGTH_TO_OCCUPIED_TEU[self.length]

@DataSummariesCache.cache_result
def get_arrival_time(self) -> datetime.datetime:

if self.cached_arrival_time is not None:
return self.cached_arrival_time

container_arrival_time: datetime.datetime
if self.delivered_by == ModeOfTransport.truck:
# noinspection PyTypeChecker
Expand All @@ -136,9 +138,11 @@ def get_arrival_time(self) -> datetime.datetime:
self.save()
return container_arrival_time

@DataSummariesCache.cache_result
def get_departure_time(self) -> datetime.datetime:

if self.cached_departure_time is not None:
return self.cached_departure_time

container_departure_time: datetime.datetime
if self.picked_up_by_truck is not None:
# noinspection PyTypeChecker
Expand Down
8 changes: 7 additions & 1 deletion conflowgen/domain_models/data_types/container_length.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class ContainerLength(enum.Enum):
other = -1 # doc: Any other length usually does not fit into the standardized slots and handling processes.

@classmethod
def get_factor(cls, container_length: ContainerLength) -> float:
def get_teu_factor(cls, container_length: ContainerLength) -> float:
"""
Each container occupies a certain amount of space when stored.
This required space is measured in TEU.
Expand All @@ -36,6 +36,10 @@ def get_factor(cls, container_length: ContainerLength) -> float:
"""
return CONTAINER_LENGTH_TO_OCCUPIED_TEU[container_length]

@classmethod
def get_maximum_teu_factor(cls) -> float:
return MAXIMUM_OCCUPIED_TEU

def __str__(self) -> str:
"""
The textual representation is, e.g., '20 feet' instead of '<ContainerLength.twenty_feet>' so it is easier to
Expand Down Expand Up @@ -79,3 +83,5 @@ def cast_element_type(cls, text: str) -> ContainerLength | None:
The TEU factor for the value 'other' is chosen to be rather large because it is assumed to be difficult to find a proper
storage position.
"""

MAXIMUM_OCCUPIED_TEU = max(list(CONTAINER_LENGTH_TO_OCCUPIED_TEU.values()))
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
from conflowgen.tools.continuous_distribution import ContinuousDistribution


class ContainerDwellTimeCouldNotBeCastedException(Exception):
pass


class ContainerDwellTimeDistributionRepository:

@staticmethod
Expand Down Expand Up @@ -68,7 +72,7 @@ def set_distributions(
elif isinstance(container_dwell_time_distribution, dict):
distribution_properties = container_dwell_time_distribution
else:
raise Exception(
raise ContainerDwellTimeCouldNotBeCastedException(
f"The container dwell time distribution representation "
f"'{container_dwell_time_distribution}' could not be casted."
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,6 @@ def get_teu_factor(cls) -> float:
container_length_weighted_average = 0.0
container_length_distribution = cls.get_distribution()
for container_length, fraction in container_length_distribution.items():
container_length_weighted_average += ContainerLength.get_factor(container_length) * fraction
container_length_weighted_average += ContainerLength.get_teu_factor(container_length) * fraction
assert 0 < container_length_weighted_average < ContainerLength.get_maximum_teu_factor()
return container_length_weighted_average
2 changes: 1 addition & 1 deletion conflowgen/domain_models/factories/container_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class ContainerFactory:
Creates containers according to the distributions which are either hard-coded or stored in the database.
"""

ignored_capacity = ContainerLength.get_factor(ContainerLength.other)
ignored_capacity = ContainerLength.get_teu_factor(ContainerLength.other)

random_seed = 1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

class LargeScheduledVehicleRepository:

ignored_capacity = ContainerLength.get_factor(ContainerLength.other)
ignored_capacity = ContainerLength.get_teu_factor(ContainerLength.other)

def __init__(self):
self.transportation_buffer = None
Expand Down Expand Up @@ -44,7 +44,7 @@ def block_capacity_for_inbound_journey(

# calculate new free capacity
free_capacity_in_teu = self.free_capacity_for_inbound_journey_buffer[vehicle]
used_capacity_in_teu = ContainerLength.get_factor(container_length=container.length)
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}, " \
Expand All @@ -65,7 +65,7 @@ def block_capacity_for_outbound_journey(

# calculate new free capacity
free_capacity_in_teu = self.free_capacity_for_outbound_journey_buffer[vehicle]
used_capacity_in_teu = ContainerLength.get_factor(container_length=container.length)
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}, " \
Expand Down Expand Up @@ -133,10 +133,10 @@ def _get_free_capacity_in_teu(
loaded_other_containers = container_counter(vehicle, ContainerLength.other)
free_capacity_in_teu = (
maximum_capacity
- loaded_20_foot_containers * ContainerLength.get_factor(ContainerLength.twenty_feet)
- loaded_40_foot_containers * ContainerLength.get_factor(ContainerLength.forty_feet)
- loaded_45_foot_containers * ContainerLength.get_factor(ContainerLength.forty_five_feet)
- loaded_other_containers * ContainerLength.get_factor(ContainerLength.other)
- 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 " \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def get_departing_vehicles(
)

# Check for each of the vehicles how much it has already loaded
required_capacity_in_teu = ContainerLength.get_factor(required_capacity)
required_capacity_in_teu = ContainerLength.get_teu_factor(required_capacity)
vehicles_with_sufficient_capacity = []
vehicle: Type[AbstractLargeScheduledVehicle]
for vehicle in vehicles:
Expand Down
12 changes: 10 additions & 2 deletions conflowgen/flow_generator/abstract_truck_for_containers_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
from ..tools.continuous_distribution import ContinuousDistribution, multiply_discretized_probability_densities


class SumOfProbabilitiesDoesNotEqualOneException(Exception):
pass


class UnknownDistributionPropertyException(Exception):
pass


class AbstractTruckForContainersManager(abc.ABC):
def __init__(self):
self.logger = logging.getLogger("conflowgen")
Expand Down Expand Up @@ -128,7 +136,7 @@ def _get_time_window_of_truck_arrival(
)

if sum(total_probabilities) == 0: # bad circumstances, no slot available
raise Exception(
raise SumOfProbabilitiesDoesNotEqualOneException(
f"No truck slots available! {truck_arrival_probabilities} and {total_probabilities} just do not match."
)

Expand All @@ -142,7 +150,7 @@ def _get_time_window_of_truck_arrival(
elif _debug_check_distribution_property == "average":
selected_time_window = int(round(container_dwell_time_distribution.average))
else:
raise Exception(f"Unknown: {_debug_check_distribution_property}")
raise UnknownDistributionPropertyException(_debug_check_distribution_property)
else:
selected_time_window = random.choices(
population=time_windows_for_truck_arrival,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

class AllocateSpaceForContainersDeliveredByTruckService:

ignored_capacity = ContainerLength.get_factor(ContainerLength.other)
ignored_capacity = ContainerLength.get_teu_factor(ContainerLength.other)

def __init__(self):
self.logger = logging.getLogger("conflowgen")
Expand Down Expand Up @@ -131,7 +131,7 @@ def allocate(self) -> None:
continue # try again (possibly new vehicle type, definitely not same vehicle again)

container = self.container_factory.create_container_for_delivering_truck(vehicle)
teu_total += ContainerLength.get_factor(container.length)
teu_total += ContainerLength.get_teu_factor(container.length)
self.large_scheduled_vehicle_repository.block_capacity_for_outbound_journey(vehicle, container)
successful_assignment += 1
break # success, no further looping to search for a suitable vehicle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def _draw_vehicle(
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)
if free_capacity >= ContainerLength.get_factor(ContainerLength.other):
if free_capacity >= ContainerLength.get_teu_factor(ContainerLength.other):
vehicles_and_their_respective_free_capacity[vehicle] = free_capacity

if len(available_vehicles) == 0:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import random
from typing import Dict, Optional

from .abstract_truck_for_containers_manager import AbstractTruckForContainersManager
from .abstract_truck_for_containers_manager import AbstractTruckForContainersManager, \
UnknownDistributionPropertyException
from ..domain_models.data_types.container_length import ContainerLength
from ..domain_models.data_types.storage_requirement import StorageRequirement
from ..domain_models.arrival_information import TruckArrivalInformationForDelivery
Expand Down Expand Up @@ -85,7 +86,7 @@ def _get_container_delivery_time(
elif _debug_check_distribution_property == "average":
random_time_component = 0
else:
raise Exception(f"Unknown: {_debug_check_distribution_property}")
raise UnknownDistributionPropertyException(f"Unknown: {_debug_check_distribution_property}")

truck_arrival_time = (
# go back to the earliest time window
Expand Down Expand Up @@ -143,6 +144,6 @@ def generate_trucks_for_delivering(self) -> None:
)
container.delivered_by_truck = truck
container.save()
teu_total += ContainerLength.get_factor(container.length)
teu_total += ContainerLength.get_teu_factor(container.length)
self.logger.info(f"All {number_containers} trucks that deliver a container are created now, moving "
f"{teu_total} TEU.")
Loading

0 comments on commit a7d44f6

Please sign in to comment.