Skip to content

Commit

Permalink
Adjust naming and add assertions for container dwell time (#136)
Browse files Browse the repository at this point in the history
* Adjust naming conventions for named tuples

* show axis names on most empty plots

* Simplify weekly distribution for truck arrivals

* add assertions for container dwell times
  • Loading branch information
1kastner authored Aug 9, 2022
1 parent 3cd6c27 commit ec8c861
Show file tree
Hide file tree
Showing 15 changed files with 454 additions and 140 deletions.
7 changes: 0 additions & 7 deletions .github/workflows/installation-from-remote.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
name: Install from repository

on:
push:
branches:
- main
pull_request:
branches:
- main
types: [opened, reopened, edited, synchronize]
schedule:
- cron: '42 23 * * 3' # every Wednesday at 23:42

Expand Down
60 changes: 37 additions & 23 deletions conflowgen/flow_generator/abstract_truck_for_containers_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,34 +67,38 @@ def _update_truck_arrival_and_container_dwell_time_distributions(
self,
hour_of_the_week_fraction_pairs: List[Union[Tuple[int, float], Tuple[int, int]]]
) -> None:
for vehicle in ModeOfTransport:
for vehicle_type in ModeOfTransport:
for storage_requirement in StorageRequirement:
container_dwell_time_distribution = self._get_container_dwell_time_distribution(
vehicle, storage_requirement
vehicle_type, storage_requirement
)

# only work with full hours
if self.is_reversed:
container_dwell_time_distribution.minimum = int(math.floor(
container_dwell_time_distribution.minimum))
container_dwell_time_distribution.maximum = int(math.ceil(
container_dwell_time_distribution.maximum))
# Adjust the minimum and maximum to harmonize with the truck slot units. While rounding, always take a
# conservative approach. The minimum and maximum values should not be exceeded!
container_dwell_time_distribution.minimum = int(math.ceil(
container_dwell_time_distribution.minimum))
container_dwell_time_distribution.maximum = int(math.floor(
container_dwell_time_distribution.maximum))

# When we talk about truck deliveries, they can come earliest at the maximum container dwell time
# and latest at the minimum. When, on the other hand, we talk about truck pickups, they can come
# earliest after container arrival and latest at the maximum dwell time.
# In both cases, less time windows are available.
if not self.is_reversed:
considered_time_window_in_hours = (
container_dwell_time_distribution.maximum
- container_dwell_time_distribution.minimum
)
else:
container_dwell_time_distribution.minimum = int(math.ceil(
container_dwell_time_distribution.minimum))
container_dwell_time_distribution.maximum = int(math.floor(
container_dwell_time_distribution.maximum))

considered_time_window_in_hours = (
container_dwell_time_distribution.maximum
- 2 # both the first and last time window are not an option
)
considered_time_window_in_hours = container_dwell_time_distribution.maximum

self.truck_arrival_distributions[vehicle][storage_requirement] = WeeklyDistribution(
self.logger.info(f"For vehicle type {vehicle_type} and storage requirement {storage_requirement}, "
"the container dwell times need to range from "
f"{container_dwell_time_distribution.minimum} to "
f"{container_dwell_time_distribution.maximum}")
self.truck_arrival_distributions[vehicle_type][storage_requirement] = WeeklyDistribution(
hour_fraction_pairs=hour_of_the_week_fraction_pairs,
considered_time_window_in_hours=considered_time_window_in_hours,
minimum_dwell_time_in_hours=container_dwell_time_distribution.minimum,
context=f"{self.__class__.__name__} : {vehicle} : {storage_requirement}"
considered_time_window_in_hours=considered_time_window_in_hours
)

def _get_distributions(
Expand Down Expand Up @@ -124,6 +128,8 @@ def _get_time_window_of_truck_arrival(
Number of hours after the earliest possible slot
"""
time_windows_for_truck_arrival = list(truck_arrival_distribution_slice.keys())
assert max(time_windows_for_truck_arrival) < container_dwell_time_distribution.maximum

truck_arrival_probabilities = list(truck_arrival_distribution_slice.values())
container_dwell_time_probabilities = container_dwell_time_distribution.get_probabilities(
time_windows_for_truck_arrival, reversed_distribution=self.is_reversed
Expand All @@ -133,10 +139,18 @@ def _get_time_window_of_truck_arrival(
container_dwell_time_probabilities
)
if sum(total_probabilities) == 0: # bad circumstances, no slot available
raise Exception(f"No slots available! {truck_arrival_probabilities} and {total_probabilities} just do not"
f"match, there is no truck available!")
raise Exception(f"No truck slots available! {truck_arrival_probabilities} and {total_probabilities} just "
"do not match.")
selected_time_window = random.choices(
population=time_windows_for_truck_arrival,
weights=total_probabilities
)[0]
if not self.is_reversed: # truck delivery of export container
assert container_dwell_time_distribution.minimum <= selected_time_window
assert selected_time_window < container_dwell_time_distribution.maximum
else: # truck pick-up of import container
assert 0 <= selected_time_window
assert selected_time_window < (container_dwell_time_distribution.maximum
- container_dwell_time_distribution.minimum)

return selected_time_window
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,6 @@ def generate(self):
self.logger.info("Assign containers that are picked up from the terminal by a vehicle adhering a schedule to "
"their specific vehicle instance...")
self.large_scheduled_vehicle_for_onward_transportation_manager.choose_departing_vehicle_for_containers()
number_assigned_containers = (self.large_scheduled_vehicle_for_onward_transportation_manager
.number_assigned_containers)
number_not_assignable_containers = (self.large_scheduled_vehicle_for_onward_transportation_manager
.number_not_assignable_containers)
assigned_as_fraction = number_assigned_containers / (
number_assigned_containers + number_not_assignable_containers
)
self.logger.info(
f"Containers for which no outgoing vehicle could be found: {assigned_as_fraction:.2%}")

self.logger.info("Loading status of vehicles adhering to a schedule:")
report = ContainerFlowStatisticsReport(transportation_buffer=self.transportation_buffer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import random
from typing import Tuple, List, Dict, Type, Sequence

# noinspection PyProtectedMember
import numpy as np
# noinspection PyProtectedMember
from peewee import fn, JOIN, ModelSelect

from ..domain_models.data_types.container_length import ContainerLength
Expand Down Expand Up @@ -34,8 +34,6 @@ def __init__(self):
self.large_scheduled_vehicle_repository = self.schedule_repository.large_scheduled_vehicle_repository
self.mode_of_transport_distribution_repository = ModeOfTransportDistributionRepository()
self.mode_of_transport_distribution = self.mode_of_transport_distribution_repository.get_distribution()
self.number_assigned_containers = 0
self.number_not_assignable_containers = 0

self.container_dwell_time_distribution_repository = ContainerDwellTimeDistributionRepository()
self.container_dwell_time_distributions: \
Expand Down Expand Up @@ -65,24 +63,24 @@ def choose_departing_vehicle_for_containers(self) -> None:
This method might be quite time-consuming because it repeatedly checks how many containers are already placed
on a vehicle to obey the load restriction (maximum capacity of the vehicle available for the terminal).
"""
self.number_assigned_containers = 0
self.number_not_assignable_containers = 0
number_assigned_containers = 0
number_not_assignable_containers = 0

self.large_scheduled_vehicle_repository.reset_cache()

self.logger.info("Assign containers to departing vehicles that move according to a schedule...")

# Get all containers in a random order which are picked up by a LargeScheduledVehicle
# This way no vehicle has an advantage over another by its earlier arrival (getting better slots etc.)
containers: ModelSelect = Container.select(
selected_containers: ModelSelect = Container.select(
).order_by(
fn.Random()
).where(
Container.picked_up_by << ModeOfTransport.get_scheduled_vehicles()
)

# all the joins just exist to speed up the process and avoid triggering too many database calls
preloaded_containers: ModelSelect = containers.join(
preloaded_containers: ModelSelect = selected_containers.join(
Truck,
join_type=JOIN.LEFT_OUTER,
on=(Container.delivered_by_truck == Truck.id)
Expand All @@ -98,20 +96,22 @@ def choose_departing_vehicle_for_containers(self) -> None:
on=(Container.delivered_by_large_scheduled_vehicle == LargeScheduledVehicle.id)
)

assert containers.count() == preloaded_containers.count(), \
selected_containers_count = selected_containers.count()
assert selected_containers_count == preloaded_containers.count(), \
f"No container should be lost due to the join operations but " \
f"{containers.count()} != {preloaded_containers.count()}"
f"{selected_containers.count()} != {preloaded_containers.count()}"

self.logger.info(f"In total, {len(preloaded_containers)} containers continue their journey on a vehicle that "
f"adhere to a schedule, assigning these containers to their respective vehicles...")
for i, container in enumerate(preloaded_containers):
i += 1
if i % 1000 == 0 and i > 0:
self.logger.info(f"Progress: {i} / {len(containers)} ({100 * i / len(containers):.2f}%) "
f"containers have been assigned to a scheduled vehicle to leave the terminal again.")
self.logger.info(
f"Progress: {i} / {len(selected_containers)} ({100 * i / len(selected_containers):.2f}%) "
f"containers have been assigned to a scheduled vehicle to leave the terminal again."
)

container_arrival = self._get_arrival_time_of_container(container)

minimum_dwell_time_in_hours, maximum_dwell_time_in_hours = self._get_dwell_times(container)

# This value has been randomly drawn during container generation for the inbound traffic.
Expand All @@ -130,23 +130,25 @@ def choose_departing_vehicle_for_containers(self) -> None:
# this is the case when there is a vehicle available - let's hope everything else works out as well!
vehicle = self._pick_vehicle_for_container(available_vehicles, container)
if vehicle is not None:
self.number_assigned_containers += 1
else:
# Well, there was a vehicle available. However, it was not suitable for our container due to
# some constraint. Maybe the container dwell time was unrealistic and thus not permissible?
# This can happen if the distribution is just really, really close to zero, so it is approximated
# as zero.
self.number_not_assignable_containers += 1
self._find_alternative_mode_of_transportation(
container, container_arrival, minimum_dwell_time_in_hours, maximum_dwell_time_in_hours
)
else:
# Maybe no permissible vehicles of the required vehicle type are left, then we need to switch the type
# as to somehow move the container out of the container yard.
self.number_not_assignable_containers += 1
self._find_alternative_mode_of_transportation(
container, container_arrival, minimum_dwell_time_in_hours, maximum_dwell_time_in_hours
)
# We are lucky and the vehicle has accepted the container for its outbound journey
number_assigned_containers += 1
continue
# No vehicle is available, either due to operational constraints or we really ran out of vehicles.
number_not_assignable_containers += 1
self._find_alternative_mode_of_transportation(
container, container_arrival, minimum_dwell_time_in_hours, maximum_dwell_time_in_hours
)

number_containers = number_assigned_containers + number_not_assignable_containers
assert number_containers == selected_containers_count, \
f"All containers should have been treated but {number_containers} != {selected_containers.count()}"
if number_containers == 0:
self.logger.info("No containers are moved from one vehicle adhering to a schedule to another one.")
else:
assigned_as_fraction = number_assigned_containers / number_containers
self.logger.info("Containers for which no outgoing vehicle adhering to a schedule could be found: "
f"{assigned_as_fraction:.2%}. These will be re-assigned to another vehicle type, "
"such as a truck.")

self.logger.info("All containers for which a departing vehicle that moves according to a schedule was "
"available have been assigned to one.")
Expand Down
51 changes: 34 additions & 17 deletions conflowgen/flow_generator/truck_for_export_containers_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,30 @@ def _get_container_delivery_time(
container: Container,
container_departure_time: datetime.datetime
) -> datetime.datetime:
"""
When was the container delivered to the terminal, given its departure time?
Args:
container: The container in question
container_departure_time: The container's departure time (fixed by a vessel or similar)
Returns:
The time a truck delivered the container to the terminal (some point before the vessel departs).
"""

container_dwell_time_distribution, truck_arrival_distribution = self._get_distributions(container)
minimum_dwell_time_in_hours = container_dwell_time_distribution.minimum
maximum_dwell_time_in_hours = container_dwell_time_distribution.maximum

earliest_slot = (
container_departure_time.replace(minute=0, second=0, microsecond=0) # reset to previous full hour
+ datetime.timedelta(hours=1) # go 1h in the future because previously we lost some minutes
- datetime.timedelta(hours=maximum_dwell_time_in_hours) # go back x hours
truck_arrival_distribution_slice = truck_arrival_distribution.get_distribution_slice(
start_as_datetime=(
container_departure_time
- datetime.timedelta(hours=maximum_dwell_time_in_hours)
)
)

truck_arrival_distribution_slice = truck_arrival_distribution.get_distribution_slice(earliest_slot)
assert max(truck_arrival_distribution_slice) < maximum_dwell_time_in_hours, \
f"{max(truck_arrival_distribution_slice)} < {maximum_dwell_time_in_hours} was harmed for container " \
f"{container}."

delivery_time_window_start = self._get_time_window_of_truck_arrival(
container_dwell_time_distribution, truck_arrival_distribution_slice
Expand All @@ -63,21 +75,26 @@ def _get_container_delivery_time(

# go back to the earliest possible day
truck_arrival_time = (
earliest_slot
# go back to the earliest time window
container_departure_time
- datetime.timedelta(hours=(maximum_dwell_time_in_hours - 1))

# add the selected time window identifier
+ datetime.timedelta(hours=delivery_time_window_start)

# spread the truck arrivals withing the time window.
+ datetime.timedelta(hours=random_time_component)
- datetime.timedelta(hours=1) # with the random time component a point close to one hour is added

# With the random time component, a point probably close to one hour is added which might harm the
# required minimum container dwell time.
- datetime.timedelta(hours=1)
)

dwell_time_in_seconds = (container_departure_time - truck_arrival_time).total_seconds()
if maximum_dwell_time_in_hours / 3600 > maximum_dwell_time_in_hours:
self.logger.debug("Maximum dwell time constraint harmed due to a rounding error, move back 1 hour")
truck_arrival_time -= datetime.timedelta(hours=0.5)
dwell_time_in_seconds = (container_departure_time - truck_arrival_time).total_seconds()
dwell_time_in_hours = (container_departure_time - truck_arrival_time).total_seconds() / 3600

assert dwell_time_in_seconds > 0, "Dwell time must be positive"
assert minimum_dwell_time_in_hours <= dwell_time_in_seconds / 3600 <= maximum_dwell_time_in_hours, \
f"{minimum_dwell_time_in_hours} <= {dwell_time_in_seconds / 3600} <= {maximum_dwell_time_in_hours} " \
assert dwell_time_in_hours > 0, "Dwell time must be positive"
assert minimum_dwell_time_in_hours <= dwell_time_in_hours <= maximum_dwell_time_in_hours, \
f"{minimum_dwell_time_in_hours} <= {dwell_time_in_hours} <= {maximum_dwell_time_in_hours} " \
f"harmed for container {container}."

return truck_arrival_time
Expand All @@ -94,7 +111,7 @@ def generate_trucks_for_delivering(self) -> None:
if i % 1000 == 0 and i > 0:
self.logger.info(
f"Progress: {i} / {len(containers)} ({100 * i / len(containers):.2f}%) trucks generated "
f"for export containers")
f"to deliver containers to the terminal.")
picked_up_with: LargeScheduledVehicle = container.picked_up_by_large_scheduled_vehicle

# assume that the vessel arrival time changes are not communicated on time so that the trucks which deliver
Expand Down
Loading

0 comments on commit ec8c861

Please sign in to comment.