# package(s) for data handling
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
# opentisim package
from opentisim.agribulk_objects import *
from opentisim import agribulk_defaults
from opentisim import core
[docs]class System:
"""This class implements the 'complete supply chain' concept (Van Koningsveld et al, 2020) for agribulk terminals.
The module allows variation of the type of quay crane used and the type of storage used.
Terminal development is governed by the following triggers:
- the allowable waiting time as a factor of service time at the berth
- the allowable dwell time of cargo in the storage area, and
- the allowable waiting time as a factor of service time at the station."""
def __init__(self, startyear=2019, lifecycle=20, operational_hours=5840, debug=False, elements=[],
crane_type_defaults=agribulk_defaults.mobile_crane_data,
storage_type_defaults=agribulk_defaults.silo_data,
allowable_waiting_service_time_ratio_berth=0.3, allowable_berth_occupancy=0.4,
allowable_dwelltime=18 / 365,
allowable_waiting_service_time_ratio_station=0.5, allowable_station_occupancy=0.4):
# time inputs
self.startyear = startyear
self.lifecycle = lifecycle
self.operational_hours = operational_hours
# provide intermediate outputs via print statements if debug = True
self.debug = debug
# collection of all terminal objects
self.elements = elements
# default values to use in case various types can be selected
self.crane_type_defaults = crane_type_defaults
self.storage_type_defaults = storage_type_defaults
# triggers for the various elements (berth, storage and station)
self.allowable_waiting_service_time_ratio_berth = allowable_waiting_service_time_ratio_berth
self.allowable_berth_occupancy = allowable_berth_occupancy
self.allowable_dwelltime = allowable_dwelltime
self.allowable_waiting_service_time_ratio_station = allowable_waiting_service_time_ratio_station
self.allowable_station_occupancy = allowable_station_occupancy
# storage variables for revenue
self.demurrage = []
self.revenues = []
# *** Overall terminal investment strategy for terminal class.
[docs] def simulate(self):
"""The 'simulate' method implements the terminal investment strategy for this terminal class.
This method automatically generates investment decisions, parametrically derived from overall demand trends and
a number of investment triggers.
Generic approaches based on:
- PIANC. 2014. Master plans for the development of existing ports. MarCom - Report 158, PIANC
- Van Koningsveld, M. (Ed.), Verheij, H., Taneja, P. and De Vriend, H.J. (in preparation). Ports and Waterways.
Navigating the changing world. TU Delft, Delft, The Netherlands.
- Van Koningsveld, M. and J. P. M. Mulder. 2004. Sustainable Coastal Policy Developments in the
Netherlands. A Systematic Approach Revealed. Journal of Coastal Research 20(2), pp. 375-385
Specific application based on (modifications have been applied where deemed an improvement):
- Ijzermans, W., 2019. Terminal design optimization. Adaptive agribulk terminal planning
in light of an uncertain future. Master's thesis. Delft University of Technology, Netherlands.
URL: http://resolver.tudelft.nl/uuid:7ad9be30-7d0a-4ece-a7dc-eb861ae5df24.
The simulate method applies Frame of Reference style decisions while stepping through each year of the terminal
lifecycle and checks if investments are needed (in light of strategic objective, operational objective,
QSC, decision recipe and intervention method):
1. for each year estimate the anticipated vessel arrivals based on the expected demand
2. for each year evaluate which investments are needed given the strategic and operational objectives
3. for each year calculate the energy costs (requires insight in realized demands)
4. for each year calculate the demurrage costs (requires insight in realized demands)
5. for each year calculate terminal revenues (requires insight in realized demands)
6. collect all cash flows (capex, opex, revenues)
7. calculate PV's and aggregate to NPV
"""
# Todo: check demurrage and revenues module
for year in range(self.startyear, self.startyear + self.lifecycle):
"""
The simulate method is designed according to the following overall objectives for the terminal:
- strategic objective: To maintain a profitable enterprise (NPV > 0) over the terminal lifecycle
- operational objective: Annually invest in infrastructure upgrades when performance criteria are triggered
"""
if self.debug:
print('')
print('### Simulate year: {} ############################'.format(year))
# 1. for each year estimate the anticipated vessel arrivals based on the expected demand
handysize, handymax, panamax, total_calls, total_vol = self.calculate_vessel_calls(year)
if self.debug:
print('--- Cargo volume and vessel calls for {} ---------'.format(year))
print(' Total cargo volume: {}'.format(total_vol))
print(' Total vessel calls: {}'.format(total_calls))
print(' Handysize calls: {}'.format(handysize))
print(' Handymax calls: {}'.format(handymax))
print(' Panamax calls: {}'.format(panamax))
print('----------------------------------------------------')
# 2. for each year evaluate which investment are needed given the strategic and operational objectives
self.berth_invest(year, handysize, handymax, panamax)
if self.debug:
print('')
print('$$$ Check quay conveyors (coupled with quay crane capacity) -----------')
self.conveyor_quay_invest(year, agribulk_defaults.quay_conveyor_data)
if self.debug:
print('')
print('$$$ Check storage (coupled with max call size and dwell time) ---------')
self.storage_invest(year, self.storage_type_defaults)
if self.debug:
print('')
print('$$$ Check hinterland conveyors (coupled with unloading stations) ------')
self.conveyor_hinter_invest(year, agribulk_defaults.hinterland_conveyor_data)
if self.debug:
print('')
print('$$$ Check unloading station (coupled with quay cranes) ----------------')
self.unloading_station_invest(year)
# 3. for each year calculate the energy costs (requires insight in realized demands)
for year in range(self.startyear, self.startyear + self.lifecycle):
self.calculate_energy_cost(year)
# 4. for each year calculate the demurrage costs (requires insight in realized demands)
self.demurrage = []
for year in range(self.startyear, self.startyear + self.lifecycle):
self.calculate_demurrage_cost(year)
# 5. for each year calculate terminal revenues (requires insight in realized demands)
self.revenues = []
for year in range(self.startyear, self.startyear + self.lifecycle):
self.calculate_revenue(year)
# 6. collect all cash flows (capex, opex, revenues)
# cash_flows, cash_flows_WACC_real = core.add_cashflow_elements(self)
# 7. calculate PV's and aggregate to NPV
core.NPV(self, Labour(**agribulk_defaults.labour_data))
# *** Individual investment methods for terminal elements
[docs] def berth_invest(self, year, handysize, handymax, panamax):
"""
Given the overall objectives for the terminal apply the following decision recipe (Van Koningsveld and Mulder,
2004) for the berth investments.
Decision recipe Berth:
QSC: berth_occupancy & allowable_waiting_service_time_ratio
Benchmarking procedure: there is a problem if the estimated berth_occupancy triggers a waiting time over
service time ratio that is larger than the allowed waiting time over service time ratio
- allowable_waiting_service_time_ratio = .30 # 30% (see PIANC (2014))
- a berth needs:
- a quay, and
- cranes (min:1 and max: max_cranes)
- berth occupancy depends on:
- total_calls, total_vol and time needed for mooring, unmooring
- total_service_capacity as delivered by the cranes
- berth occupancy in combination with nr of berths is used to lookup the waiting over service time ratio
Intervention procedure: invest enough to make the planned waiting service time ratio < allowable waiting
service time ratio
- adding berths, quays and cranes decreases berth_occupancy_rate (and increases the number of servers)
which will yield a smaller waiting time over service time ratio
"""
# report on the status of all berth elements
if self.debug:
print('')
print('--- Status terminal @ start of year ----------------')
core.report_element(self, Berth, year)
core.report_element(self, Quay_wall, year)
core.report_element(self, Cyclic_Unloader, year)
core.report_element(self, Continuous_Unloader, year)
core.report_element(self, Conveyor_Quay, year)
core.report_element(self, Storage, year)
core.report_element(self, Conveyor_Hinter, year)
core.report_element(self, Unloading_station, year)
# calculate planned berth occupancy and planned nr of berths
berth_occupancy_planned, berth_occupancy_online, crane_occupancy_planned, crane_occupancy_online = \
self.calculate_berth_occupancy(year, handysize, handymax, panamax)
berths = len(core.find_elements(self, Berth))
# get the waiting time as a factor of service time
if berths != 0:
planned_waiting_service_time_ratio_berth = core.occupancy_to_waitingfactor(
utilisation=berth_occupancy_planned, nr_of_servers_to_chk=berths)
else:
planned_waiting_service_time_ratio_berth = np.inf
if self.debug:
print(' Berth occupancy online (@ start of year): {:.2f} (trigger level: {:.2f})'.format(berth_occupancy_online, self.allowable_berth_occupancy))
print(' Berth occupancy planned (@ start of year): {:.2f} (trigger level: {:.2f})'.format(berth_occupancy_planned, self.allowable_berth_occupancy))
print(' Planned waiting time service time factor (@ start of year): {:.2f} (trigger level: {:.2f})'.format(planned_waiting_service_time_ratio_berth, self.allowable_waiting_service_time_ratio_berth))
print('')
print('--- Start investment analysis ----------------------')
print('')
print('$$$ Check berth elements (coupled with berth occupancy) ---------------')
core.report_element(self, Berth, year)
core.report_element(self, Quay_wall, year)
core.report_element(self, Cyclic_Unloader, year)
# while planned_waiting_service_time_ratio is larger than self.allowable_waiting_service_time_ratio_berth
while planned_waiting_service_time_ratio_berth > self.allowable_waiting_service_time_ratio_berth:
# while planned waiting service time ratio is too large add a berth when no crane slots are available
if not (self.check_crane_slot_available()):
if self.debug:
print(' *** add Berth to elements')
berth = Berth(**agribulk_defaults.berth_data)
berth.year_online = year + berth.delivery_time
self.elements.append(berth)
berths = len(core.find_elements(self, Berth))
berth_occupancy_planned, berth_occupancy_online, crane_occupancy_planned, crane_occupancy_online = \
self.calculate_berth_occupancy(year, handysize, handymax, panamax)
planned_waiting_service_time_ratio_berth = core.occupancy_to_waitingfactor(
utilisation=berth_occupancy_planned, nr_of_servers_to_chk=berths)
if self.debug:
print(' Berth occupancy planned (after adding berth): {:.2f} (trigger level: {:.2f})'.format(
berth_occupancy_planned, self.allowable_berth_occupancy))
print(' Planned waiting time service time factor : {:.2f} (trigger level: {:.2f})'.format(
planned_waiting_service_time_ratio_berth, self.allowable_waiting_service_time_ratio_berth))
# while planned waiting service time ratio is too large add a berth if a quay is needed
berths = len(core.find_elements(self, Berth))
quay_walls = len(core.find_elements(self, Quay_wall))
if berths > quay_walls:
# bug fixed, should only take the value of the vessels that actually come
# Todo: make sure that also other commodities are included
length_v = max(
(not agribulk_defaults.maize_data['handysize_perc'] == 0) * agribulk_defaults.handysize_data["LOA"],
(not agribulk_defaults.maize_data['handymax_perc'] == 0) * agribulk_defaults.handymax_data["LOA"],
(not agribulk_defaults.maize_data['panamax_perc'] == 0) * agribulk_defaults.panamax_data["LOA"]) # max size
draft = max(
(not agribulk_defaults.maize_data['handysize_perc'] == 0) * agribulk_defaults.handysize_data["draft"],
(not agribulk_defaults.maize_data['handymax_perc'] == 0) * agribulk_defaults.handymax_data["draft"],
(not agribulk_defaults.maize_data['panamax_perc'] == 0) * agribulk_defaults.panamax_data["draft"])
# apply PIANC 2014:
# see Ijzermans, 2019 - infrastructure.py line 107 - 111
if quay_walls == 0:
# - length when next quay is n = 1
length = length_v + 2 * 15 # ref: PIANC 2014
elif quay_walls == 1:
# - length when next quay is n > 1
length = 1.1 * berths * (length_v + 15) - (length_v + 2 * 15) # ref: PIANC 2014
else:
length = 1.1 * berths * (length_v + 15) - 1.1 * (berths - 1) * (length_v + 15)
# - depth
quay_wall = Quay_wall(**agribulk_defaults.quay_wall_data)
depth = np.sum([draft, quay_wall.max_sinkage, quay_wall.wave_motion, quay_wall.safety_margin])
self.quay_invest(year, length, depth)
berth_occupancy_planned, berth_occupancy_online, crane_occupancy_planned, crane_occupancy_online = \
self.calculate_berth_occupancy(year, handysize, handymax, panamax)
planned_waiting_service_time_ratio_berth = core.occupancy_to_waitingfactor(
utilisation=berth_occupancy_planned, nr_of_servers_to_chk=berths)
if self.debug:
print(' Berth occupancy planned (after adding berth): {:.2f} (trigger level: {:.2f})'.format(
berth_occupancy_planned, self.allowable_berth_occupancy))
print(' Planned waiting time service time factor : {:.2f} (trigger level: {:.2f})'.format(
planned_waiting_service_time_ratio_berth, self.allowable_waiting_service_time_ratio_berth))
# while planned berth occupancy is too large add a crane if a crane is needed
if self.check_crane_slot_available():
self.crane_invest(year)
berth_occupancy_planned, berth_occupancy_online, crane_occupancy_planned, crane_occupancy_online = \
self.calculate_berth_occupancy(year, handysize, handymax, panamax)
planned_waiting_service_time_ratio_berth = core.occupancy_to_waitingfactor(
utilisation=berth_occupancy_planned, nr_of_servers_to_chk=berths)
if self.debug:
print(' Berth occupancy planned (after adding berth): {:.2f} (trigger level: {:.2f})'.format(
berth_occupancy_planned, self.allowable_berth_occupancy))
print(' Planned waiting time service time factor : {:.2f} (trigger level: {:.2f})'.format(
planned_waiting_service_time_ratio_berth, self.allowable_waiting_service_time_ratio_berth))
[docs] def quay_invest(self, year, length, depth):
"""
Given the overall objectives for the terminal apply the following decision recipe (Van Koningsveld and
Mulder, 2004) for the quay investments.
Decision recipe Quay:
QSC: quay_per_berth
Benchmarking procedure (triggered in self.berth_invest): there is a problem when
the number of berths > the number of quays, but also while the planned waiting over service time ratio is
too large
Intervention procedure: invest enough to make sure that each quay has a berth and the planned waiting over
service time ratio is below the max allowable waiting over service time ratio
- adding quay will increase quay_per_berth
- quay_wall.length must be long enough to accommodate largest expected vessel
- quay_wall.depth must be deep enough to accommodate largest expected vessel
- quay_wall.freeboard must be high enough to accommodate largest expected vessel
"""
if self.debug:
print(' *** add Quay to elements')
# add a Quay_wall element
quay_wall = Quay_wall(**agribulk_defaults.quay_wall_data)
# add length and depth to the elements (useful for later reporting)
quay_wall.length = length
quay_wall.depth = depth
quay_wall.retaining_height = 2 * (depth + quay_wall.freeboard)
# - capex
quay_wall.unit_rate = int(quay_wall.Gijt_constant_2 * 2 * (depth + quay_wall.freeboard))
mobilisation = int(max((length * quay_wall.unit_rate * quay_wall.mobilisation_perc), quay_wall.mobilisation_min))
# Todo: consider adding cost of apron and cost of land here (compare containers)
quay_wall.capex = int(length * quay_wall.unit_rate + mobilisation)
# - opex
quay_wall.insurance = quay_wall.unit_rate * length * quay_wall.insurance_perc
quay_wall.maintenance = quay_wall.unit_rate * length * quay_wall.maintenance_perc
quay_wall.year_online = year + quay_wall.delivery_time
# - land use
# Todo: consider adding a landuse section here (compare containers)
# add cash flow information to quay_wall object in a dataframe
quay_wall = core.add_cashflow_data_to_element(self, quay_wall)
self.elements.append(quay_wall)
[docs] def crane_invest(self, year):
"""
Given the overall objectives for the terminal apply the following decision recipe (Van Koningsveld and
Mulder, 2004) for the crane investments.
Decision recipe Crane:
QSC: planned waiting over service time ratio
Benchmarking procedure (triggered in self.berth_invest): there is a problem when the planned planned
waiting over service time ratio is larger than the max allowable waiting over service time ratio
Intervention procedure: invest until planned waiting over service time ratio is below the max allowable
waiting over service time ratio
"""
if self.debug:
print(' *** add Harbour crane to elements')
# add unloader object
if (self.crane_type_defaults["crane_type"] == 'Gantry crane' or
self.crane_type_defaults["crane_type"] == 'Harbour crane' or
self.crane_type_defaults["crane_type"] == 'Mobile crane'):
crane = Cyclic_Unloader(**self.crane_type_defaults)
elif self.crane_type_defaults["crane_type"] == 'Screw unloader':
crane = Continuous_Unloader(**self.crane_type_defaults)
# - capex
unit_rate = crane.unit_rate
mobilisation = unit_rate * crane.mobilisation_perc
crane.capex = int(unit_rate + mobilisation)
# - opex
crane.insurance = unit_rate * crane.insurance_perc
crane.maintenance = unit_rate * crane.maintenance_perc
# - labour
labour = Labour(**agribulk_defaults.labour_data)
crane.shift = ((crane.crew * self.operational_hours) / (
labour.shift_length * labour.annual_shifts))
crane.labour = crane.shift * labour.operational_salary
# apply proper timing for the crane to come online (in the same year as the latest Quay_wall)
years_online = []
for element in core.find_elements(self, Quay_wall):
years_online.append(element.year_online)
crane.year_online = max([year + crane.delivery_time, max(years_online)])
# add cash flow information to quay_wall object in a dataframe
crane = core.add_cashflow_data_to_element(self, crane)
# add object to elements
self.elements.append(crane)
[docs] def conveyor_quay_invest(self, year, agribulk_defaults_quay_conveyor_data):
"""
Given the overall objectives for the terminal apply the following decision recipe (Van Koningsveld and
Mulder, 2004) for the quay conveyor investments.
Operational objective: maintain a quay conveyor capacity that at least matches the quay crane capacity (so
basically the quay conveyors follow what happens on the berth)
Decision recipe quay conveyor:
QSC: quay_conveyor_capacity planned
Benchmarking procedure: there is a problem when the quay_conveyor_capacity_planned is smaller than the
quay_crane_service_rate_planned
For the quay conveyor investments the strategy is to at least match the quay crane processing capacity
Intervention procedure: the intervention strategy is to add quay conveyors until the trigger is achieved
- find out how much quay_conveyor_capacity is planned
- find out how much quay_crane_service_rate is planned
- add quay_conveyor_capacity until it matches quay_crane_service_rate
"""
# find the total quay_conveyor capacity
quay_conveyor_capacity_planned = 0
quay_conveyor_capacity_online = 0
list_of_elements = core.find_elements(self, Conveyor_Quay)
if list_of_elements != []:
# Todo: check if 'if isinstance(element, Conveyor_Quay):' is more efficient
for element in list_of_elements:
quay_conveyor_capacity_planned += element.capacity_steps
if year >= element.year_online:
quay_conveyor_capacity_online += element.capacity_steps
if self.debug:
print(' a total of {} ton of quay conveyor service capacity is online; {} ton still pending'.format(
quay_conveyor_capacity_online, quay_conveyor_capacity_planned-quay_conveyor_capacity_online))
# find the total quay crane service rate,
quay_crane_service_rate_planned = 0
years_online = []
for element in (core.find_elements(self, Cyclic_Unloader) + core.find_elements(self, Continuous_Unloader)):
quay_crane_service_rate_planned += element.peak_capacity
years_online.append(element.year_online)
# check if total planned capacity of the quay conveyor is smaller than planned capacity of the quay cranes,
# if so add a conveyor
while quay_conveyor_capacity_planned < quay_crane_service_rate_planned:
if self.debug:
print(' *** add Quay Conveyor to elements')
conveyor_quay = Conveyor_Quay(**agribulk_defaults_quay_conveyor_data)
# - capex
capacity = conveyor_quay.capacity_steps
unit_rate = conveyor_quay.unit_rate_factor * conveyor_quay.length
mobilisation = conveyor_quay.mobilisation
conveyor_quay.capex = int(capacity * unit_rate + mobilisation)
# - opex
conveyor_quay.insurance = capacity * unit_rate * conveyor_quay.insurance_perc
conveyor_quay.maintenance = capacity * unit_rate * conveyor_quay.maintenance_perc
# - labour
labour = Labour(**agribulk_defaults.labour_data)
conveyor_quay.shift = (
(conveyor_quay.crew * self.operational_hours) / (labour.shift_length * labour.annual_shifts))
conveyor_quay.labour = conveyor_quay.shift * labour.operational_salary
# apply proper timing for the crane to come online (in the same year as the latest Quay_wall)
new_crane_years = [x for x in years_online if x >= year]
# find the maximum online year of Conveyor_Quay or make it []
if core.find_elements(self, Conveyor_Quay) != []:
max_conveyor_years = max([x.year_online for x in core.find_elements(self, Conveyor_Quay)])
else:
max_conveyor_years = []
# decide what online year to use
if max_conveyor_years == []:
conveyor_quay.year_online = min(new_crane_years)
elif max_conveyor_years < min(new_crane_years):
conveyor_quay.year_online = min(new_crane_years)
elif max_conveyor_years == min(new_crane_years):
conveyor_quay.year_online = max(new_crane_years)
elif max_conveyor_years > min(new_crane_years):
conveyor_quay.year_online = max(new_crane_years)
# add cash flow information to quay_wall object in a dataframe
conveyor_quay = core.add_cashflow_data_to_element(self, conveyor_quay)
self.elements.append(conveyor_quay)
quay_conveyor_capacity_planned += conveyor_quay.capacity_steps
if self.debug:
print(' a total of {} ton of conveyor quay service capacity is online; {} ton still pending'.format(
quay_conveyor_capacity_online, quay_conveyor_capacity_planned-quay_conveyor_capacity_online))
[docs] def storage_invest(self, year, agribulk_defaults_storage_data):
"""
Given the overall objectives for the terminal apply the following decision recipe (Van Koningsveld and
Mulder, 2004) for the storage investments.
Operational objective: maintain a storage capacity that is large enough to at least contain one time the largest
vessel call size or that is large enough to accommodate a maximum allowable dwell time plus 10 percent.
Decision recipe storage:
QSC: storage_capacity
Benchmarking procedure: there is a problem when the storage_capacity is too small to store one time the
largest call size or when it is too small to allow for a predetermined max allowable dwell time
The max allowable dwell time is here determined as 5% of the annual demand, increased by 10% (PIANC, 2014)
Intervention procedure: the intervention strategy is to add storage until the benchmarking trigger is
achieved. The trigger is the max of one call size, or the volume derived from the dwell time requirement.
"""
# from all storage objects sum online capacity
storage_capacity = 0
storage_capacity_online = 0
list_of_elements = core.find_elements(self, Storage)
if list_of_elements != []:
for element in list_of_elements:
if element.type == agribulk_defaults_storage_data['type']:
storage_capacity += element.capacity
if year >= element.year_online:
storage_capacity_online += element.capacity
if self.debug:
print(' a total of {} ton of {} storage capacity is online; {} ton still pending'.format(
storage_capacity_online, agribulk_defaults_storage_data['type'], storage_capacity-storage_capacity_online))
handysize, handymax, panamax, total_calls, total_vol = self.calculate_vessel_calls(year)
berth_occupancy_planned, berth_occupancy_online, crane_occupancy_planned, crane_occupancy_online = \
self.calculate_berth_occupancy(year, handysize, handymax, panamax)
# here an important bug was fixed! Previous code took the max call size of all vessels,
# but it needs to take the max call size of the vessels that actually arrive
max_vessel_call_size = 0
for vessel in core.find_elements(self, Vessel):
if vessel.type == 'Handysize' and handysize != 0:
max_vessel_call_size = max(vessel.call_size, max_vessel_call_size)
if vessel.type == 'Handymax' and handymax != 0:
max_vessel_call_size = max(vessel.call_size, max_vessel_call_size)
if vessel.type == 'Panamax' and panamax != 0:
max_vessel_call_size = max(vessel.call_size, max_vessel_call_size)
# find the total service rate,
service_rate = 0
for element in (core.find_elements(self, Cyclic_Unloader) + core.find_elements(self, Continuous_Unloader)):
if year >= element.year_online:
service_rate += element.effective_capacity * crane_occupancy_online
commodities = core.find_elements(self, Commodity)
if commodities != []:
for commodity in commodities:
volume = commodity.scenario_data.loc[commodity.scenario_data['year'] == year]['volume'].item()
storage_capacity_dwelltime = round((volume * 0.05) * 1.1) # see IJzermans (2019) p.26 & PIANC (2014) p.148
# check if sufficient storage capacity is available
while storage_capacity < max(max_vessel_call_size, storage_capacity_dwelltime):
if self.debug:
print(' *** add storage to elements')
# add storage object
storage = Storage(**agribulk_defaults_storage_data)
# - capex
storage.capex = storage.unit_rate * storage.capacity + storage.mobilisation_min
# - opex
storage.insurance = storage.unit_rate * storage.capacity * storage.insurance_perc
storage.maintenance = storage.unit_rate * storage.capacity * storage.maintenance_perc
# - labour
labour = Labour(**agribulk_defaults.labour_data)
storage.shift = ((storage.crew * self.operational_hours) / (labour.shift_length * labour.annual_shifts))
storage.labour = storage.shift * labour.operational_salary
if year == self.startyear:
storage.year_online = year + storage.delivery_time + 1
else:
storage.year_online = year + storage.delivery_time
# add cash flow information to quay_wall object in a dataframe
storage = core.add_cashflow_data_to_element(self, storage)
self.elements.append(storage)
storage_capacity += storage.capacity
if self.debug:
print(' a total of {} ton of {} storage capacity is online; {} ton still pending'.format(
storage_capacity_online, agribulk_defaults_storage_data['type'],
storage_capacity - storage_capacity_online))
[docs] def unloading_station_invest(self, year):
"""The operational objective for the investment strategy for unloading stations is to have sufficient planned
unloading stations to keep the station occupancy below a given threshold for the quay crane capacity planned.
current strategy is to add unloading stations as soon as a service trigger is achieved
- find out how much service capacity is online
- find out how much service capacity is planned
- find out how much service capacity is needed
- add service capacity until service_trigger is no longer exceeded
"""
station_occupancy_planned, station_occupancy_online = self.calculate_station_occupancy(year)
train_calls = self.train_call(year)
unloading_stations = len(core.find_elements(self, Unloading_station))
# get the waiting time as a factor of service time
if unloading_stations != 0:
planned_waiting_service_time_ratio_station = core.occupancy_to_waitingfactor(
utilisation=station_occupancy_planned, nr_of_servers_to_chk=unloading_stations)
else:
planned_waiting_service_time_ratio_station = np.inf
if self.debug:
print(' Station occupancy planned (@ start of year): {:.2f}'.format(station_occupancy_planned))
print(' Waiting factor (@ start of year): {:.2f}'.format(planned_waiting_service_time_ratio_station))
print(' Number of stations planned (@start of year): {:.2f}'.format(unloading_stations))
print(' Number of trains (@start of year): {:.2f}'.format(train_calls))
# todo: check this trigger
# Ijzemans (2019): "In the end, based on reference projects in eastern Europe, the loading bay was modelled
# using queuing theory and an assumed allowable train waiting time equal to 50 % of service time."
while planned_waiting_service_time_ratio_station > self.allowable_waiting_service_time_ratio_station:
# add a station when station occupancy is too high
if self.debug:
print(' *** add unloading station to elements')
station = Unloading_station(**agribulk_defaults.hinterland_station_data)
# - capex
unit_rate = station.unit_rate
mobilisation = station.mobilisation
station.capex = int(unit_rate + mobilisation)
# - opex
station.insurance = unit_rate * station.insurance_perc
station.maintenance = unit_rate * station.maintenance_perc
# - labour
labour = Labour(**agribulk_defaults.labour_data)
station.shift = ((station.crew * self.operational_hours) / (labour.shift_length * labour.annual_shifts))
station.labour = station.shift * labour.operational_salary
if year == self.startyear:
station.year_online = year + station.delivery_time + 1
else:
station.year_online = year + station.delivery_time
# add cash flow information to quay_wall object in a dataframe
station = core.add_cashflow_data_to_element(self, station)
self.elements.append(station)
station_occupancy_planned, station_occupancy_online = self.calculate_station_occupancy(year)
unloading_stations = len(core.find_elements(self, Unloading_station))
planned_waiting_service_time_ratio_station = core.occupancy_to_waitingfactor(
utilisation=station_occupancy_planned, nr_of_servers_to_chk=unloading_stations)
[docs] def conveyor_hinter_invest(self, year, agribulk_defaults_hinterland_conveyor_data):
"""
Given the overall objectives for the terminal apply the following decision recipe (Van Koningsveld and Mulder,
2004) for the hinter conveyor investments.
Operational objective: maintain a hinter conveyor capacity that at least matches the station unloading capacity
(so basically the hinter conveyors follow what happens on the station)
Decision recipe quay conveyor:
QSC: hinter_conveyor_capacity planned
Benchmarking procedure: there is a problem when the hinter_conveyor_capacity_planned is smaller than the
station_service_rate_planned
For the hinter conveyor investments the strategy is to at least match the unloading station capacity
Intervention procedure: the intervention strategy is to add hinter conveyors until the trigger is achieved
- find out how much hinter_conveyor_capacity is planned
- find out how much station_service_rate_planned is planned
- add hinter_conveyor_capacity until it matches station_service_rate_planned
"""
# find the total service rate
hinter_conveyor_capacity_planned = 0
hinter_conveyor_capacity_online = 0
list_of_elements_conveyor = core.find_elements(self, Conveyor_Hinter)
if list_of_elements_conveyor != []:
for element in list_of_elements_conveyor:
hinter_conveyor_capacity_planned += element.capacity_steps
if year >= element.year_online:
hinter_conveyor_capacity_online += element.capacity_steps
if self.debug:
print(
' a total of {} ton of hinterland conveyor service capacity is online; {} ton still pending'.format(
hinter_conveyor_capacity_online,
hinter_conveyor_capacity_planned - hinter_conveyor_capacity_online))
# find the total station service rate planned,
station_service_rate_planned = 0
years_online = []
for element in (core.find_elements(self, Unloading_station)):
station_service_rate_planned += element.production
years_online.append(element.year_online)
# check if the hinter conveyor capacity (planned) at least matches the station unloading rate (planned)
while hinter_conveyor_capacity_planned < station_service_rate_planned:
if self.debug:
print(' *** add Hinter Conveyor to elements')
conveyor_hinter = Conveyor_Hinter(**agribulk_defaults_hinterland_conveyor_data)
# - capex
capacity = conveyor_hinter.capacity_steps
unit_rate = conveyor_hinter.unit_rate_factor * conveyor_hinter.length
mobilisation = conveyor_hinter.mobilisation
conveyor_hinter.capex = int(capacity * unit_rate + mobilisation)
# - opex
conveyor_hinter.insurance = capacity * unit_rate * conveyor_hinter.insurance_perc
conveyor_hinter.maintenance = capacity * unit_rate * conveyor_hinter.maintenance_perc
# - labour
labour = Labour(**agribulk_defaults.labour_data)
conveyor_hinter.shift = (
(conveyor_hinter.crew * self.operational_hours) / (labour.shift_length * labour.annual_shifts))
conveyor_hinter.labour = conveyor_hinter.shift * labour.operational_salary
# - online year
conveyor_hinter.year_online = max(years_online)
# add cash flow information to quay_wall object in a dataframe
conveyor_hinter = core.add_cashflow_data_to_element(self, conveyor_hinter)
self.elements.append(conveyor_hinter)
hinter_conveyor_capacity_planned += conveyor_hinter.capacity_steps
if self.debug:
print(
' a total of {} ton of hinterland conveyor service capacity is online; {} ton still pending'.format(
hinter_conveyor_capacity_online,
hinter_conveyor_capacity_planned - hinter_conveyor_capacity_online))
# *** Various cost calculation methods
[docs] def calculate_energy_cost(self, year):
"""
1. calculate the value of the total demand in year (demand * handling fee)
2. calculate the maximum amount that can be handled (service capacity * operational hours)
Terminal.revenues is the minimum of 1. and 2.
"""
energy = Energy(**agribulk_defaults.energy_data)
handysize, handymax, panamax, total_calls, total_vol = self.calculate_vessel_calls(year)
berth_occupancy_planned, berth_occupancy_online, crane_occupancy_planned, crane_occupancy_online = self.calculate_berth_occupancy(
year, handysize, handymax, panamax)
station_occupancy_planned, station_occupancy_online = self.calculate_station_occupancy(year)
# calculate crane energy
list_of_elements_1 = core.find_elements(self, Cyclic_Unloader)
list_of_elements_2 = core.find_elements(self, Continuous_Unloader)
list_of_elements_Crane = list_of_elements_1 + list_of_elements_2
for element in list_of_elements_Crane:
if year >= element.year_online:
consumption = element.consumption
hours = self.operational_hours * crane_occupancy_online
if consumption * hours * energy.price != np.inf:
element.df.loc[element.df['year'] == year, 'energy'] = consumption * hours * energy.price
else:
element.df.loc[element.df['year'] == year, 'energy'] = 0
# calculate Quay conveyor energy
list_of_elements_quay = core.find_elements(self, Conveyor_Quay)
for element in list_of_elements_quay:
if year >= element.year_online:
consumption = element.capacity_steps * element.consumption_coefficient + element.consumption_constant
hours = self.operational_hours * crane_occupancy_online
if consumption * hours * energy.price != np.inf:
element.df.loc[element.df['year'] == year, 'energy'] = consumption * hours * energy.price
else:
element.df.loc[element.df['year'] == year, 'energy'] = 0
# calculate storage energy
list_of_elements_Storage = core.find_elements(self, Storage)
for element in list_of_elements_Storage:
if year >= element.year_online:
consumption = element.consumption
capacity = element.capacity
hours = self.operational_hours
if consumption * capacity * hours * energy.price != np.inf:
element.df.loc[element.df['year'] == year, 'energy'] = consumption * capacity * hours * energy.price
else:
element.df.loc[element.df['year'] == year, 'energy'] = 0
# calculate hinterland conveyor energy
list_of_elements_hinter = core.find_elements(self, Conveyor_Hinter)
for element in list_of_elements_hinter:
if year >= element.year_online:
consumption = element.capacity_steps * element.consumption_coefficient + element.consumption_constant
hours = self.operational_hours * station_occupancy_online
if consumption * hours * energy.price != np.inf:
element.df.loc[element.df['year'] == year, 'energy'] = consumption * hours * energy.price
else:
element.df.loc[element.df['year'] == year, 'energy'] = 0
# calculate hinterland station energy
station_occupancy_planned, station_occupancy_online = self.calculate_station_occupancy(year)
list_of_elements_Station = core.find_elements(self, Unloading_station)
for element in list_of_elements_Station:
if year >= element.year_online:
if element.consumption * self.operational_hours * station_occupancy_online * energy.price != np.inf:
element.df.loc[element.df['year'] == year, 'energy'
] = element.consumption * self.operational_hours * station_occupancy_online * energy.price
else:
element.df.loc[element.df['year'] == year, 'energy'] = 0
[docs] def calculate_demurrage_cost(self, year):
"""Find the demurrage cost per type of vessel and sum all demurrage cost"""
handysize_calls, handymax_calls, panamax_calls, total_calls, total_vol = self.calculate_vessel_calls(year)
berth_occupancy_planned, berth_occupancy_online, crane_occupancy_planned, crane_occupancy_online = \
self.calculate_berth_occupancy(year, handysize_calls, handymax_calls, panamax_calls)
berths = len(core.find_elements(self, Berth))
waiting_factor = \
core.occupancy_to_waitingfactor(utilisation=berth_occupancy_online, nr_of_servers_to_chk=berths)
waiting_time_hours = waiting_factor * crane_occupancy_online * self.operational_hours / total_calls
waiting_time_occupancy = waiting_time_hours * total_calls / self.operational_hours
# Find the service_rate per quay_wall to find the average service hours at the quay for a vessel
quay_walls = len(core.find_elements(self, Quay_wall))
service_rate = 0
for element in (core.find_elements(self, Cyclic_Unloader) + core.find_elements(self, Continuous_Unloader)):
if year >= element.year_online:
service_rate += element.effective_capacity / quay_walls
# Find the demurrage cost per type of vessel
if service_rate != 0:
handymax = Vessel(**agribulk_defaults.handymax_data)
service_time_handymax = handymax.call_size / service_rate
waiting_time_hours_handymax = waiting_factor * service_time_handymax
port_time_handymax = waiting_time_hours_handymax + service_time_handymax + handymax.mooring_time
penalty_time_handymax = max(0, port_time_handymax - handymax.all_turn_time)
demurrage_time_handymax = penalty_time_handymax * handymax_calls
demurrage_cost_handymax = demurrage_time_handymax * handymax.demurrage_rate
handysize = Vessel(**agribulk_defaults.handysize_data)
service_time_handysize = handysize.call_size / service_rate
waiting_time_hours_handysize = waiting_factor * service_time_handysize
port_time_handysize = waiting_time_hours_handysize + service_time_handysize + handysize.mooring_time
penalty_time_handysize = max(0, port_time_handysize - handysize.all_turn_time)
demurrage_time_handysize = penalty_time_handysize * handysize_calls
demurrage_cost_handysize = demurrage_time_handysize * handysize.demurrage_rate
panamax = Vessel(**agribulk_defaults.panamax_data)
service_time_panamax = panamax.call_size / service_rate
waiting_time_hours_panamax = waiting_factor * service_time_panamax
port_time_panamax = waiting_time_hours_panamax + service_time_panamax + panamax.mooring_time
penalty_time_panamax = max(0, port_time_panamax - panamax.all_turn_time)
demurrage_time_panamax = penalty_time_panamax * panamax_calls
demurrage_cost_panamax = demurrage_time_panamax * panamax.demurrage_rate
else:
demurrage_cost_handymax = 0
demurrage_cost_handysize = 0
demurrage_cost_panamax = 0
total_demurrage_cost = demurrage_cost_handymax + demurrage_cost_handysize + demurrage_cost_panamax
self.demurrage.append(total_demurrage_cost)
[docs] def calculate_revenue(self, year):
"""
1. calculate the value of the total demand in year (demand * handling fee)
2. calculate the maximum amount that can be handled (service capacity * operational hours)
Terminal.revenues is the minimum of 1. and 2.
"""
# implement a safety factor
quay_walls = len(core.find_elements(self, Quay_wall))
crane_cyclic = len(core.find_elements(self, Cyclic_Unloader))
crane_continuous = len(core.find_elements(self, Continuous_Unloader))
conveyor_quay = len(core.find_elements(self, Conveyor_Quay))
storage = len(core.find_elements(self, Storage))
conveyor_hinter = len(core.find_elements(self, Conveyor_Hinter))
station = len(core.find_elements(self, Unloading_station))
if quay_walls < 1 and conveyor_quay < 1 and (
crane_cyclic > 1 or crane_continuous > 1) and storage < 1 and conveyor_hinter < 1 and station < 1:
safety_factor = 0
else:
safety_factor = 1
# gather volumes from each commodity, calculate how much revenue it would yield, and add
revenues = 0
for commodity in core.find_elements(self, Commodity):
fee = commodity.handling_fee
try:
volume = commodity.scenario_data.loc[commodity.scenario_data['year'] == year]['volume'].item()
revenues += (volume * fee * safety_factor)
except:
pass
if self.debug:
print(' Revenues (potential - given demand): {:.2f}'.format(revenues))
handysize, handymax, panamax, total_calls, total_vol = self.calculate_vessel_calls(year)
berth_occupancy_planned, berth_occupancy_online, crane_occupancy_planned, crane_occupancy_online = self.calculate_berth_occupancy(
year, handysize, handymax, panamax)
# find the total service rate,
service_rate = 0
for element in (core.find_elements(self, Cyclic_Unloader) + core.find_elements(self, Continuous_Unloader)):
if year >= element.year_online:
service_rate += element.effective_capacity * crane_occupancy_online
# find the rate between volume and throughput
rate_throughput_volume = service_rate * self.operational_hours / total_vol
if self.debug:
print(' Revenues (realised - throughput): {}'.format(
int(service_rate * self.operational_hours * fee * safety_factor)))
try:
self.revenues.append(
min(revenues * safety_factor, service_rate * self.operational_hours * fee * safety_factor))
except:
pass
# todo: check if rest value is included at the end of the simulation
# *** General functions
[docs] def calculate_vessel_calls(self, year=2019):
"""Calculate volumes to be transported and the number of vessel calls (both per vessel type and in total) """
# intialize values to be returned
handysize_vol = 0
handymax_vol = 0
panamax_vol = 0
total_vol = 0
# gather volumes from each commodity scenario and calculate how much is transported with which vessel
commodities = core.find_elements(self, Commodity)
for commodity in commodities:
try:
volume = commodity.scenario_data.loc[commodity.scenario_data['year'] == year]['volume'].item()
handysize_vol += volume * commodity.handysize_perc / 100
handymax_vol += volume * commodity.handymax_perc / 100
panamax_vol += volume * commodity.panamax_perc / 100
total_vol += volume
except:
pass
# gather vessels and calculate the number of calls each vessel type needs to make
vessels = core.find_elements(self, Vessel)
for vessel in vessels:
if vessel.type == 'Handysize':
handysize_calls = int(np.ceil(handysize_vol / vessel.call_size))
elif vessel.type == 'Handymax':
handymax_calls = int(np.ceil(handymax_vol / vessel.call_size))
elif vessel.type == 'Panamax':
panamax_calls = int(np.ceil(panamax_vol / vessel.call_size))
total_calls = np.sum([handysize_calls, handymax_calls, panamax_calls])
return handysize_calls, handymax_calls, panamax_calls, total_calls, total_vol
[docs] def calculate_berth_occupancy(self, year, handysize_calls, handymax_calls, panamax_calls):
"""
- Find all cranes and sum their effective_capacity to get service_capacity
- Divide callsize_per_vessel by service_capacity and add mooring time to get total time at berth
- Occupancy is total_time_at_berth divided by operational hours
"""
# intialize values to be returned
total_vol = 0
# gather volumes from each commodity scenario
commodities = core.find_elements(self, Commodity)
for commodity in commodities:
try:
volume = commodity.scenario_data.loc[commodity.scenario_data['year'] == year]['volume'].item()
total_vol += volume
except:
pass
# list all crane objects in system
list_of_elements_1 = core.find_elements(self, Cyclic_Unloader)
list_of_elements_2 = core.find_elements(self, Continuous_Unloader)
list_of_elements = list_of_elements_1 + list_of_elements_2
# find the total service rate and determine the time at berth (in hours, per vessel type and in total)
service_rate_planned = 0
service_rate_online = 0
if list_of_elements != []: # when cranes are at least planned
for element in list_of_elements:
service_rate_planned += element.effective_capacity
if year >= element.year_online:
service_rate_online += element.effective_capacity
# calculate mooring and unmooring times for each vessel type
time_at_berth_planned_handysize = handysize_calls * agribulk_defaults.handysize_data["mooring_time"]
time_at_berth_planned_handymax = handymax_calls * agribulk_defaults.handymax_data["mooring_time"]
time_at_berth_planned_panamax = panamax_calls * agribulk_defaults.panamax_data["mooring_time"]
# calculate the time that the cranes require to load/unload the commodity
time_at_cranes_planned = total_vol / service_rate_planned
# add mooring/unmooring and loading/unloading times
total_time_at_berth_planned = np.sum([
time_at_cranes_planned,
time_at_berth_planned_handysize,
time_at_berth_planned_handymax,
time_at_berth_planned_panamax])
# berth_occupancy is the total time at berth divided by the operational hours
berth_occupancy_planned = total_time_at_berth_planned / self.operational_hours
crane_occupancy_planned = time_at_cranes_planned / self.operational_hours
if service_rate_online != 0: # when some cranes are actually online
# calculate mooring and unmooring times for each vessel type
time_at_berth_online_handysize = handysize_calls * agribulk_defaults.handysize_data["mooring_time"]
time_at_berth_online_handymax = handymax_calls * agribulk_defaults.handymax_data["mooring_time"]
time_at_berth_online_panamax = panamax_calls * agribulk_defaults.panamax_data["mooring_time"]
# calculate the time that the cranes require to load/unload the commodity
time_at_cranes_online = total_vol / service_rate_online
# add mooring/unmooring and loading/unloading times
total_time_at_berth_online = np.sum([
time_at_cranes_online,
time_at_berth_online_handysize,
time_at_berth_online_handymax,
time_at_berth_online_panamax])
# berth_occupancy is the total time at berth divided by the operational hours
berth_occupancy_online = min([total_time_at_berth_online / self.operational_hours, 1])
crane_occupancy_online = min([time_at_cranes_online / self.operational_hours, 1])
else:
berth_occupancy_online = float("inf")
crane_occupancy_online = float("inf")
else:
# if there are no cranes the berth occupancy is 'infinite' so a berth is certainly needed
berth_occupancy_planned = float("inf")
berth_occupancy_online = float("inf")
crane_occupancy_planned = float("inf")
crane_occupancy_online = float("inf")
return berth_occupancy_planned, berth_occupancy_online, crane_occupancy_planned, crane_occupancy_online
[docs] def calculate_station_occupancy(self, year):
"""The station occupancy is calculated based on the service rate for the throughput of the online quay unloaders
(effective capacity * occupancy). The unloading station should at least be able to handle the throughput by the
online quay unloaders at a level that the station occupancy planned remains below the target occupancy level."""
list_of_elements = core.find_elements(self, Unloading_station)
# find the total service rate and determine the time at station
service_rate_planned = 0
service_rate_online = 0
if list_of_elements != []:
# find planned service rate and online service rate
for element in list_of_elements:
service_rate_planned += element.service_rate
if year >= element.year_online:
service_rate_online += element.service_rate
handysize, handymax, panamax, total_calls, total_vol = self.calculate_vessel_calls(year)
berth_occupancy_planned, berth_occupancy_online, crane_occupancy_planned, crane_occupancy_online = \
self.calculate_berth_occupancy(year, handysize, handymax, panamax)
# find the service rate for the throughput of the online quay unloaders (effective capacity * occupancy)
service_rate_throughput = 0
for element in (core.find_elements(self, Cyclic_Unloader) + core.find_elements(self, Continuous_Unloader)):
if year >= element.year_online:
service_rate_throughput += element.effective_capacity * crane_occupancy_online
# determine time at stations planned (given the current throughput and planned service rate)
if service_rate_planned != 0:
time_at_station_planned = service_rate_throughput * self.operational_hours / service_rate_planned # element.service_rate
station_occupancy_planned = time_at_station_planned / self.operational_hours
else:
station_occupancy_planned = float("inf")
# determine time at stations online (given the current throughput and online service rate)
if service_rate_online != 0:
time_at_station_online = service_rate_throughput * self.operational_hours / service_rate_online # element.capacity
station_occupancy_online = time_at_station_online / self.operational_hours
else:
station_occupancy_online = float("inf")
else:
# if there are no unloading stations the station occupancy is 'infinite' so a station is certainly needed
station_occupancy_planned = float("inf")
station_occupancy_online = float("inf")
return station_occupancy_planned, station_occupancy_online
[docs] def check_crane_slot_available(self):
# find number of available crane slots
list_of_elements = core.find_elements(self, Berth)
slots = 0
for element in list_of_elements:
slots += element.max_cranes
# create a list of all quay unloaders
list_of_elements_1 = core.find_elements(self, Cyclic_Unloader)
list_of_elements_2 = core.find_elements(self, Continuous_Unloader)
list_of_elements = list_of_elements_1 + list_of_elements_2
# when there are more available slots than installed cranes there are still slots available (True)
if slots > len(list_of_elements):
return True
else:
return False
[docs] def train_call(self, year):
"""Calculation of the train calls per year, this is calculated from:
- find out how much throughput there is
- find out how much cargo the train can transport
- calculate the numbers of train calls"""
# create default station object (to get the train call size)
station = Unloading_station(**agribulk_defaults.hinterland_station_data)
# - Trains calculated with the throughput
handysize, handymax, panamax, total_calls, total_vol = self.calculate_vessel_calls(year)
berth_occupancy_planned, berth_occupancy_online, crane_occupancy_planned, crane_occupancy_online = \
self.calculate_berth_occupancy(year, handysize, handymax, panamax)
service_rate_throughput_online = 0
for element in (core.find_elements(self, Cyclic_Unloader) + core.find_elements(self, Continuous_Unloader)):
if year >= element.year_online:
service_rate_throughput_online += element.effective_capacity * crane_occupancy_online
train_calls = service_rate_throughput_online * self.operational_hours / station.call_size
return train_calls
# *** Plotting functions
[docs] def terminal_elements_plot(self, width=0.1, alpha=0.6, fontsize=20, demand_step=100_000):
"""Gather data from Terminal and plot which elements come online when"""
# collect elements to add to plot
years = []
berths = []
quays = []
cranes = []
conveyors_quay = []
storages = []
conveyors_hinterland = []
unloading_station = []
matplotlib.rcParams.update({'font.size': fontsize})
for year in range(self.startyear, self.startyear + self.lifecycle):
years.append(year)
berths.append(0)
quays.append(0)
cranes.append(0)
conveyors_quay.append(0)
storages.append(0)
conveyors_hinterland.append(0)
unloading_station.append(0)
for element in self.elements:
if isinstance(element, Berth):
if year >= element.year_online:
berths[-1] += 1
if isinstance(element, Quay_wall):
if year >= element.year_online:
quays[-1] += 1
if isinstance(element, Cyclic_Unloader) | isinstance(element, Continuous_Unloader):
if year >= element.year_online:
cranes[-1] += 1
if isinstance(element, Conveyor_Quay):
if year >= element.year_online:
conveyors_quay[-1] += 1
if isinstance(element, Storage):
if year >= element.year_online:
storages[-1] += 1
if isinstance(element, Conveyor_Hinter):
if year >= element.year_online:
conveyors_hinterland[-1] += 1
if isinstance(element, Unloading_station):
if year >= element.year_online:
unloading_station[-1] += 1
# generate plot
fig, ax1 = plt.subplots(figsize=(20, 12))
ax1.grid(zorder=0, which='major', axis='both')
colors = ['firebrick', 'darksalmon', 'sandybrown', 'darkkhaki', 'palegreen', 'lightseagreen', 'mediumpurple',
'mediumvioletred', 'lightgreen']
offset = 3 * width
ax1.bar([x - offset + 0 * width for x in years], berths, zorder=1, width=width, alpha=alpha, label="Berths", color=colors[0], edgecolor='darkgrey')
ax1.bar([x - offset + 1 * width for x in years], quays, zorder=1, width=width, alpha=alpha, label="Quays", color=colors[1], edgecolor='darkgrey')
ax1.bar([x - offset + 2 * width for x in years], cranes, zorder=1, width=width, alpha=alpha, label="Cranes", color=colors[2], edgecolor='darkgrey')
ax1.bar([x - offset + 3 * width for x in years], conveyors_quay, zorder=1, width=width, alpha=alpha, label="Quay converyors", color=colors[3], edgecolor='darkgrey')
ax1.bar([x - offset + 4 * width for x in years], storages, zorder=1, width=width, alpha=alpha, label="Storages", color=colors[4], edgecolor='darkgrey')
ax1.bar([x - offset + 5 * width for x in years], conveyors_hinterland, zorder=1, width=width, alpha=alpha, label="Hinterland conveyors", color=colors[5], edgecolor='darkgrey')
ax1.bar([x - offset + 6 * width for x in years], unloading_station, zorder=1, width=width, alpha=alpha, label="Unloading stations", color=colors[6], edgecolor='darkgrey')
# added vertical lines for mentioning the different phases
# plt.axvline(x=2025.6, color='k', linestyle='--')
# plt.axvline(x=2023.4, color='k', linestyle='--')
# get demand
demand = pd.DataFrame()
demand['year'] = list(range(self.startyear, self.startyear + self.lifecycle))
demand['demand'] = 0
for commodity in core.find_elements(self, Commodity):
try:
for column in commodity.scenario_data.columns:
if column in commodity.scenario_data.columns and column != "year":
demand['demand'] += commodity.scenario_data[column]
except:
pass
# #Adding the throughput
# years = []
# throughputs_online = []
#
# for year in range(self.startyear, self.startyear + self.lifecycle):
# years.append(year)
# throughputs_online.append(0)
#
# throughput_online, throughput_planned, throughput_planned_jetty, throughput_planned_pipej, throughput_planned_storage, throughput_planned_h2retrieval, throughput_planned_pipeh = self.throughput_elements(
# year)
#
# for element in self.elements:
# if isinstance(element, Berth):
# if year >= element.year_online:
# throughputs_online[-1] = throughput_online
# Making a second graph
ax2 = ax1.twinx()
ax2.step(years, demand['demand'].values, zorder=2, label="Demand [t/y]", where='mid', color='blue')
# ax2.step(years, throughputs_online, label="Throughput [t/y]", where='mid', color='#aec7e8')
# added boxes
# props = dict(boxstyle='round', facecolor='white', alpha=0.5)
# # place a text box in upper left in axes coords
# ax1.text(0.30, 0.60, 'phase 1', transform=ax1.transAxes, fontsize=18, bbox=props)
# ax1.text(0.55, 0.60, 'phase 2', transform=ax1.transAxes, fontsize=18, bbox=props)
# ax1.text(0.82, 0.60, 'phase 3', transform=ax1.transAxes, fontsize=18, bbox=props)
# title and labels
ax1.set_title('Terminal elements online', fontsize=fontsize)
ax1.set_xlabel('Years', fontsize=fontsize)
ax1.set_ylabel('Terminal elements on line [nr]', fontsize=fontsize)
ax2.set_ylabel('Demand/throughput[t/y]', fontsize=fontsize)
# ticks and tick labels
ax1.set_xticks([x for x in years])
ax1.set_xticklabels([int(x) for x in years], rotation='vertical', fontsize=fontsize)
max_elements = max([max(berths), max(quays), max(cranes),
max(conveyors_quay), max(storages),
max(conveyors_hinterland), max(conveyors_hinterland)])
ax1.set_yticks([x for x in range(0, max_elements + 1 + 2, 2)])
ax1.set_yticklabels([int(x) for x in range(0, max_elements + 1 + 2, 2)], fontsize=fontsize)
ax2.set_yticks([x for x in range(0, np.max(demand["demand"]) + demand_step, demand_step)])
ax2.set_yticklabels([int(x) for x in range(0, np.max(demand["demand"]) + demand_step, demand_step)], fontsize=fontsize)
# print legend
fig.legend(loc='lower center', bbox_to_anchor=(0, -.01, .9, 0.7),
fancybox=True, shadow=True, ncol=5, fontsize=fontsize)
fig.subplots_adjust(bottom=0.18)
[docs] def terminal_capacity_plot(self, width=0.25, alpha=0.6, fontsize=20):
"""Gather data from Terminal and plot which elements come online when"""
# get crane service capacity and storage capacity
years = []
cranes = []
cranes_capacity = []
storages = []
storages_capacity = []
for year in range(self.startyear, self.startyear + self.lifecycle):
years.append(year)
cranes.append(0)
cranes_capacity.append(0)
storages.append(0)
storages_capacity.append(0)
handysize_calls, handymax_calls, panamax_calls, total_calls, total_vol = self.calculate_vessel_calls(year)
berth_occupancy_planned, berth_occupancy_online, crane_occupancy_planned, crane_occupancy_online = self.calculate_berth_occupancy(
year, handysize_calls, handymax_calls, panamax_calls)
for element in self.elements:
if isinstance(element, Cyclic_Unloader) | isinstance(element, Continuous_Unloader):
# calculate cranes service capacity: effective_capacity * operational hours * berth_occupancy?
if year >= element.year_online:
cranes[-1] += 1
cranes_capacity[
-1] += element.effective_capacity * self.operational_hours * crane_occupancy_online
if isinstance(element, Storage):
if year >= element.year_online:
storages[-1] += 1
storages_capacity[-1] += element.capacity * 365 / 18
# get demand
demand = pd.DataFrame()
demand['year'] = list(range(self.startyear, self.startyear + self.lifecycle))
demand['demand'] = 0
for commodity in core.find_elements(self, Commodity):
try:
for column in commodity.scenario_data.columns:
if column in commodity.scenario_data.columns and column != "year":
demand['demand'] += commodity.scenario_data[column]
except:
pass
# generate plot
fig, ax = plt.subplots(figsize=(20, 10))
ax.bar([x - 0.5 * width for x in years], cranes_capacity, width=width, alpha=alpha, label="cranes capacity",
color='red')
ax.step(years, demand['demand'].values, label="demand", where='mid')
# title and labels
ax.set_title('Terminal capacity online ({})'.format(self.crane_type_defaults['crane_type']), fontsize=fontsize)
ax.set_xlabel('Years', fontsize=fontsize)
ax.set_ylabel('Throughput capacity [tons/year]', fontsize=fontsize)
# ticks and tick labels
ax.set_xticks([x for x in years])
ax.set_xticklabels([int(x) for x in years], rotation='vertical', fontsize=fontsize)
ax.yaxis.set_tick_params(labelsize=fontsize)
# print legend
fig.legend(loc='lower center', bbox_to_anchor=(0, -.01, .9, 0.7),
fancybox=True, shadow=True, ncol=4)
fig.subplots_adjust(bottom=0.18)