Source code for opentisim.container_system

# package(s) for data handling
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt


# opentisim package
from opentisim.container_objects import *
from opentisim import container_defaults
from opentisim import core


[docs]class System: """This class implements the 'complete supply chain' concept (Van Koningsveld et al, 2020) for container terminals. The module allows variation of the type of quay crane used and the type of quay crane used and the type of stack equipment used. Terminal development is governed by the following triggers: - the allowable waiting time as a factor of service time at the berth, and - the distribution ratios (adding up to 1) for: - ladens - empties - reefers - out of gauges - the transhipment ratio """ def __init__(self, terminal_name='Terminal', startyear=2019, lifecycle=20, operational_hours=7500, debug=False, elements=[], crane_type_defaults=container_defaults.sts_crane_data, stack_equipment='rs', laden_stack='rs', allowable_waiting_service_time_ratio_berth=0.1, allowable_berth_occupancy=0.6, laden_perc=0.80, reefer_perc=0.1, empty_perc=0.05, oog_perc=0.05, transhipment_ratio=0.69, energy_price=0.17, fuel_price=1, land_price=0): # identity self.terminal_name = terminal_name # 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.stack_equipment = stack_equipment self.laden_stack = laden_stack # 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 # container split self.laden_perc = laden_perc self.reefer_perc = reefer_perc self.empty_perc = empty_perc self.oog_perc = oog_perc # modal split self.transhipment_ratio = transhipment_ratio # fuel and electrical power price self.energy_price = energy_price self.fuel_price = fuel_price self.land_price = land_price # storage variables for revenue # self.revenues = [] # input testing: throughput type percentages should add up to 1 np.testing.assert_equal(self.laden_perc + self.reefer_perc + self.empty_perc + self.oog_perc, 1, 'error: throughput type fractions should add up to 1') # *** 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: - Quist, P. and Wijdeven, B., 2014. Ports & Terminals Hand-out. Chapter 7 Container terminals. CIE4330/CIE5306 - PIANC. 2014. Master plans for the development of existing ports. MarCom - Report 158, PIANC - PIANC. 2014b. Design principles for small and medium marine containter terminals. MarCom - Report 135, PIANC - Van Koningsveld, M. (Ed.), Verheij, H., Taneja, P. and De Vriend, H.J. (2020). 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): - Koster, P.H.F., 2019. Concept level container terminal design. Investigating the consequences of accelerating the concept design phase by modelling the automatable tasks. Master's thesis. Delft University of Technology, Netherlands. URL: http://resolver.tudelft.nl/uuid:131133bf-9021-4d67-afcb-233bd8302ce0. - Stam, H.W.B., 2020. Offshore-Onshore Port Systems. A framework for the financial evaluation of offshore container terminals. Master's thesis. Delft University of Technology, Netherlands. The simulate method applies frame of reference style decisions while stepping through each year of the terminal lifecycle and check if investment is needed (in light of strategic objective, operational objective, QSC, decision recipe, intervention method): 1. for each year estimate the anticipated vessel arrivals based on the expected demand 2. for each year evaluate which investment 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 """ 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 fully_cellular_calls, panamax_calls, panamax_max_calls, post_panamax_I_calls, post_panamax_II_calls, \ new_panamax_calls, VLCS_calls, ULCS_calls, 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(' Fully cellular calls: {}'.format(fully_cellular_calls)) print(' Panamax calls: {}'.format(panamax_calls)) print(' Panamax max calls: {}'.format(panamax_max_calls)) print(' Post Panamax I calls: {}'.format(post_panamax_I_calls)) print(' Post Panamax II calls: {}'.format(post_panamax_II_calls)) print(' New Panamax calls: {}'.format(new_panamax_calls)) print(' VLCS calls: {}'.format(VLCS_calls)) print(' ULCS calls: {}'.format(ULCS_calls)) print('----------------------------------------------------') # self.access_channel_invest(year, fully_cellular, panamax, panamax_max, post_panamax_I, post_panamax_II, new_panamax, VLCS, ULCS) # 2. for each year evaluate which investment are needed given the strategic and operational objectives self.berth_invest(year, fully_cellular_calls, panamax_calls, panamax_max_calls, post_panamax_I_calls, post_panamax_II_calls, new_panamax_calls, VLCS_calls, ULCS_calls) if self.debug: print('') print('$$$ Check horizontal transport (coupled with quay crane presence) -----------') self.horizontal_transport_invest(year) if self.debug: print('') print('$$$ Check laden and reefer stack investments (coupled with demand) ----------') self.laden_reefer_stack_invest(year) if self.debug: print('') print('$$$ Check empty stack investments (coupled with demand) ---------------------') self.empty_stack_invest(year) if self.debug: print('') print('$$$ Check oog stack investments (coupled with demand) -----------------------') self.oog_stack_invest(year) if self.debug: print('') print('$$$ Check stacking equipment investment (coupled with quay crane presence) --') self.stack_equipment_invest(year) if self.debug: print('') print('$$$ Check empty handlers (coupled with quay crane presence) -----------------') self.empty_handler_invest(year) if self.debug: print('') print('$$$ Check gate investments (coupled with quay crane presence) ---------------') self.gate_invest(year) if self.debug: print('') print('$$$ Check general services --------------------------------------------------') self.general_services_invest(year) # 3. for each year calculate the general labour, fuel and energy costs (requires insight in realized demands) for year in range(self.startyear, self.startyear + self.lifecycle): self.calculate_energy_cost(year) for year in range(self.startyear, self.startyear + self.lifecycle): self.calculate_general_labour_cost(year) for year in range(self.startyear, self.startyear + self.lifecycle): self.calculate_fuel_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) # Todo: see if here a method can be implemented to estimate the revenue that is needed to get NPV = 0 # 5. for each year calculate terminal revenues # 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 = self.add_cashflow_elements() # 7. calculate key numbers # Todo: check to see if core method can be used in stead # df = core.NPV(self, Labour(**container_defaults.labour_data)) # print(df) # NPV, capex_normal, opex_normal, labour_normal = self.NPV() # 8. calculate land use total_land_use = self.calculate_land_use(year)
# Todo: implement a return method for Simulate() # land = total_land_use # labour = labour_normal[-1] # opex = opex_normal[-1] # capex_normal = np.nan_to_num(capex_normal) # capex = np.sum(capex_normal) # # data = {"equipment": self.stack_equipment, # "cost_land": self.land_price, # "cost_fuel": self.fuel_price, # "cost_power": self.energy_price, # "land": land, # "labour": labour, # "opex": opex, # "capex": capex, # "NPV": NPV} # # # Todo: check if this statement is indeed obsolete # # cash_flows, cash_flows_WACC_real = self.add_cashflow_elements() # # # Todo: this return statement should be obsolete as everything is logged in the Terminal object # return NPV, data # *** Individual investment methods for terminal elements
[docs] def berth_invest(self, year, fully_cellular, panamax, panamax_max, post_panamax_I, post_panamax_II, new_panamax, VLCS, ULCS): """ 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 = .10 # 10% (see PIANC (2014, 2014b)) - a berth needs: - a quay - 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) # Todo: check if more elements should be reported here # 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, fully_cellular, panamax, panamax_max, post_panamax_I, post_panamax_II, new_panamax, VLCS, ULCS) 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 # see also PIANC (2014b), p. 58/59 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(**container_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, fully_cellular, panamax, panamax_max, post_panamax_I, post_panamax_II, new_panamax, VLCS, ULCS) 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 Ls_max = max([ int(not container_defaults.container_data['fully_cellular_perc'] == 0) * container_defaults.fully_cellular_data["LOA"], int(not container_defaults.container_data['panamax_perc'] == 0) * container_defaults.panamax_data["LOA"], int(not container_defaults.container_data['panamax_max_perc'] == 0) * container_defaults.panamax_max_data["LOA"], int(not container_defaults.container_data['post_panamax_I_perc'] == 0) * container_defaults.post_panamax_I_data["LOA"], int(not container_defaults.container_data['post_panamax_II_perc'] == 0) * container_defaults.post_panamax_II_data["LOA"], int(not container_defaults.container_data['new_panamax_perc'] == 0) * container_defaults.new_panamax_data["LOA"], int(not container_defaults.container_data['VLCS_perc'] == 0) * container_defaults.VLCS_data["LOA"], int(not container_defaults.container_data['ULCS_perc'] == 0) * container_defaults.ULCS_data["LOA"] ]) # max size draught = max([ int(not container_defaults.container_data['fully_cellular_perc'] == 0) * container_defaults.fully_cellular_data["draught"], int(not container_defaults.container_data['panamax_perc'] == 0) * container_defaults.panamax_data["draught"], int(not container_defaults.container_data['panamax_max_perc'] == 0) * container_defaults.panamax_max_data["draught"], int(not container_defaults.container_data['post_panamax_I_perc'] == 0) * container_defaults.post_panamax_I_data["draught"], int(not container_defaults.container_data['post_panamax_II_perc'] == 0) * container_defaults.post_panamax_II_data["draught"], int(not container_defaults.container_data['new_panamax_perc'] == 0) * container_defaults.new_panamax_data["draught"], int(not container_defaults.container_data['VLCS_perc'] == 0) * container_defaults.VLCS_data["draught"], int(not container_defaults.container_data['ULCS_perc'] == 0) * container_defaults.ULCS_data["draught"] ]) # max draught Ls_avg = (fully_cellular * container_defaults.fully_cellular_data["LOA"] + panamax * container_defaults.panamax_data["LOA"] + panamax_max * container_defaults.panamax_max_data["LOA"] + post_panamax_I * container_defaults.post_panamax_I_data["LOA"] + post_panamax_II * container_defaults.post_panamax_II_data["LOA"] + new_panamax * container_defaults.new_panamax_data["LOA"] + VLCS * container_defaults.VLCS_data["LOA"] + ULCS * container_defaults.ULCS_data["LOA"]) / \ (fully_cellular + panamax + panamax_max + post_panamax_I + post_panamax_II + new_panamax + VLCS + ULCS) # apply PIANC 2014: berthing_gap = container_defaults.quay_wall_data["berthing_gap"] if quay_walls == 0: # - length when next quay is n = 1 # Lq = Ls,max + (2 x 15) ref: PIANC 2014, p 98 length = Ls_max + 2 * berthing_gap else: # - length when next quay is n > 1 # Lq = 1.1 x n x (Ls,avg+15) + 15 ref: PIANC 2014, p 98 length = 1.1 * berths * (Ls_avg + berthing_gap) + berthing_gap # - depth quay_wall = Quay_wall(**container_defaults.quay_wall_data) depth = np.sum([draught, 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, fully_cellular, panamax, panamax_max, post_panamax_I, post_panamax_II, new_panamax, VLCS, ULCS) 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, fully_cellular, panamax, panamax_max, post_panamax_I, post_panamax_II, new_panamax, VLCS, ULCS) 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(**container_defaults.quay_wall_data) # add length and depth to the elements (useful for later reporting) quay_wall.length = length quay_wall.depth = depth # draught + max_sinkage + wave_motion + safety_margin quay_wall.retaining_height = 2 * (depth + quay_wall.freeboard) # - capex # Todo: check this unit rate estimate quay_wall.unit_rate = int(quay_wall.Gijt_constant * quay_wall.retaining_height ** quay_wall.Gijt_coefficient) mobilisation = int(max((length * quay_wall.unit_rate * quay_wall.mobilisation_perc), quay_wall.mobilisation_min)) apron_pavement = length * quay_wall.apron_width * quay_wall.apron_pavement cost_of_land = length * quay_wall.apron_width * self.land_price quay_wall.capex = int(length * quay_wall.unit_rate + mobilisation + apron_pavement + cost_of_land) # - 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 quay_wall.land_use = length * quay_wall.apron_width # 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 STS 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"] == 'STS crane' or self.crane_type_defaults["crane_type"] == 'Mobile crane'): crane = Cyclic_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(**container_defaults.labour_data) crane.shift = crane.crew * labour.daily_shifts crane.labour = crane.shift * labour.blue_collar_salary # Todo: check if the number of shifts (crane.shift) is modelled correctly # 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 horizontal_transport_invest(self, year): """current strategy is to add horizontal transport (tractors) as soon as a service trigger is achieved - find out how many cranes are online and planned - find out how many tractors trailers are online and planned (each STS needs a pre-set number of tractors trailers) - add tractor trailers until the required amount (given by the cranes) is achieved """ # check the number of cranes cranes_planned = 0 cranes_online = 0 list_of_elements = core.find_elements(self, Cyclic_Unloader) if list_of_elements != []: for element in list_of_elements: cranes_planned += 1 if year >= element.year_online: cranes_online += 1 # check the number of horizontal transporters hor_transport_planned = 0 hor_transport_online = 0 list_of_elements = core.find_elements(self, Horizontal_Transport) if list_of_elements != []: for element in list_of_elements: hor_transport_planned += 1 if year >= element.year_online: hor_transport_online += 1 if self.debug: print(' Number of STS cranes online (@start of year): {}'.format(cranes_online)) print(' Number of STS cranes planned (@start of year): {}'.format(cranes_planned)) print(' Horizontal transport online (@ start of year): {}'.format(hor_transport_online)) print(' Horizontal transport planned (@ start of year): {}'.format(hor_transport_planned)) # object needs to be instantiated here so that tractor.required may be determined tractor = Horizontal_Transport(**container_defaults.tractor_trailer_data) # when the total number of online horizontal transporters < total number of transporters required by the cranes while cranes_planned * tractor.required > hor_transport_planned: # add a tractor to elements if self.debug: print(' *** add tractor trailer to elements') tractor = Horizontal_Transport(**container_defaults.tractor_trailer_data) # - capex unit_rate = tractor.unit_rate mobilisation = tractor.mobilisation tractor.capex = int(unit_rate + mobilisation) # - opex # Todo: shouldn't the tractor also be insured? tractor.maintenance = unit_rate * tractor.maintenance_perc # - labour labour = Labour(**container_defaults.labour_data) # Todo: check if the number of shifts is calculated properly tractor.shift = tractor.crew * labour.daily_shifts tractor.labour = tractor.shift * labour.blue_collar_salary # apply proper timing for the crane to come online (in the same year as the latest Quay_wall) # [element.year_online for element in core.find_elements(self, Quay_wall)] # years_online = [] # for element in core.find_elements(self, Quay_wall): # years_online.append(element.year_online) years_online = [element.year_online for element in core.find_elements(self, Cyclic_Unloader)] tractor.year_online = max([year + tractor.delivery_time, max(years_online)]) # add cash flow information to tractor object in a dataframe tractor = core.add_cashflow_data_to_element(self, tractor) self.elements.append(tractor) hor_transport_planned += 1 if self.debug: print(' a total of {} tractor trailers is online; {} tractor trailers still pending'.format( hor_transport_online, hor_transport_planned - hor_transport_online))
[docs] def laden_reefer_stack_invest(self, year): """current strategy is to add stacks as soon as trigger is achieved - find out how much stack capacity is planned - find out how much stack capacity is required - add stack capacity until service_trigger is no longer exceeded The laden stack has a number of positions for laden containers and a number of positions for reefer containers """ # calculate the required stack capacity once stack_capacity_planned, total_capacity_required, reefer_ground_slots = self.laden_reefer_stack_capacity(year) if self.debug: print(' Stack capacity planned (@ start of year): {:.2f}'.format(stack_capacity_planned)) print(' Stack capacity required (@ start of year): {:.2f}'.format(total_capacity_required)) # Required capacity should be ≤ Stack capacity planned. # While this is not the case, add stacks (PIANC (2014b), p63) while total_capacity_required > stack_capacity_planned: if self.debug: print(' *** add laden / reefer stack to elements') if self.laden_stack == 'rtg': # Rubber Tired Gantry Crane stack = Laden_Stack(**container_defaults.rtg_stack_data) elif self.laden_stack == 'rmg': # Rail Mounted Gantry Crane stack = Laden_Stack(**container_defaults.rmg_stack_data) elif self.laden_stack == 'sc': # Straddle Carrier stack = Laden_Stack(**container_defaults.sc_stack_data) elif self.laden_stack == 'rs': # Reach Stacker stack = Laden_Stack(**container_defaults.rs_stack_data) # - per stack that is added determine the land use # alternative calculation method (same result): # stack.length * stack.width * stack.gross_tgs * stack.area_factor # TEU * TEU * area per ground slot * equipment related area factor stack.land_use = (stack.capacity / stack.height) * stack.gross_tgs * stack.area_factor pavement = stack.pavement drainage = stack.drainage # - per stack that is added determine the number of reefer slots (needed for reefer_rack capex) reefer_slots = (self.reefer_perc / (self.laden_perc + self.reefer_perc)) * stack.capacity reefer_racks = reefer_slots * stack.reefer_rack # - capex stack.capex = int( (stack.land_use + pavement + drainage) * self.land_price + stack.mobilisation + reefer_racks) # - opex stack.maintenance = int((stack.land_use + pavement + drainage) * stack.maintenance_perc) # apply proper timing for the crane to come online # stack comes online in year + delivery time, or the same year as the last quay wall (whichever is largest) years_online = [element.year_online for element in core.find_elements(self, Quay_wall)] stack.year_online = max([year + stack.delivery_time, max(years_online)]) # add cash flow information to quay_wall object in a dataframe stack = core.add_cashflow_data_to_element(self, stack) self.elements.append(stack) stack_capacity_planned, total_capacity_required, reefer_ground_slots = self.laden_reefer_stack_capacity(year)
[docs] def laden_reefer_stack_capacity(self, year): """Calculate the stack capacity for laden and reefer containers""" # find the total planned and online laden stack capacity list_of_elements = core.find_elements(self, Laden_Stack) stack_capacity_planned = 0 stack_capacity_online = 0 for element in list_of_elements: stack_capacity_planned += element.capacity if year >= element.year_online: stack_capacity_online += element.capacity # determine the on-terminal total TEU/year for every throughput type (types: ladens, reefers, empties, oogs) laden_teu, reefer_teu, empty_teu, oog_teu = self.throughput_characteristics(year) # Transhipment containers are counted twice in berth throughput calculations – once off the ship and once on the # ship – but are counted only once in the yard capacity calculations. PIANC (2014b), p 63 # total positions = half of the amount that it transhipped + the full amount of what is not transhipped ts = self.transhipment_ratio laden_teu = (laden_teu * ts * 0.5) + (laden_teu * (1 - ts)) reefer_teu = (reefer_teu * ts * 0.5) + (reefer_teu * (1 - ts)) # instantiate laden, reefer and stack objects (needed to get properties) laden = Container(**container_defaults.laden_container_data) reefer = Container(**container_defaults.reefer_container_data) if self.laden_stack == 'rtg': # Rubber Tired Gantry crane stack = Laden_Stack(**container_defaults.rtg_stack_data) elif self.laden_stack == 'rmg': # Rail Mounted Gantry crane stack = Laden_Stack(**container_defaults.rmg_stack_data) elif self.laden_stack == 'sc': # Straddle Carrier stack = Laden_Stack(**container_defaults.sc_stack_data) elif self.laden_stack == 'rs': # Reach Stacker stack = Laden_Stack(**container_defaults.rs_stack_data) else: stack = Laden_Stack(**container_defaults.rtg_stack_data) # calculate operational days operational_days = self.operational_hours / 24 # determine laden and reefer ground slots (see Quist and Wijdeven (2014) p. 49) # throughput demand (corrected for ts) x peak factor x dwell times = total nr of containers to be stacked # total nr of containers to be stacked divided by stack height times nr of operational days times stack # occupancy increases nr of containers to be stacked and leads to the number of ground slots you need laden_ground_slots = ((laden_teu * laden.peak_factor * laden.dwell_time) /\ (stack.height * laden.stack_occupancy * operational_days)) # for the nr of reefer ground slots you need to multiply by a reefer factor (stack sorting factor) reefer_ground_slots = (reefer_teu * reefer.peak_factor * reefer.dwell_time) /\ (stack.height * reefer.stack_occupancy * stack.reefer_factor * operational_days) # total nr of ground slots total_ground_slots = laden_ground_slots + reefer_ground_slots # determine capacity (nr ground slots x height) total_capacity_required = total_ground_slots * stack.height return stack_capacity_planned, total_capacity_required, reefer_ground_slots
[docs] def empty_stack_invest(self, year): """current strategy is to add stacks as soon as trigger is achieved - find out how much stack capacity is online - find out how much stack capacity is planned - find out how much stack capacity is needed - add stack capacity until service_trigger is no longer exceeded """ empty_capacity_planned, empty_capacity_required = self.empty_stack_capacity(year) if self.debug: print(' Empty stack capacity planned (@ start of year): {:.2f}'.format(empty_capacity_planned)) print(' Empty stack capacity required (@ start of year): {:.2f}'.format(empty_capacity_required)) while empty_capacity_required > empty_capacity_planned: if self.debug: print(' *** add empty stack to elements') empty_stack = Empty_Stack(**container_defaults.empty_stack_data) # - land use stack_ground_slots = empty_stack.capacity / empty_stack.height empty_stack.land_use = stack_ground_slots * empty_stack.gross_tgs * empty_stack.area_factor # - capex area = empty_stack.length * empty_stack.width gross_tgs = empty_stack.gross_tgs pavement = empty_stack.pavement drainage = empty_stack.drainage area_factor = empty_stack.area_factor mobilisation = empty_stack.mobilisation cost_of_land = self.land_price empty_stack.capex = int( (pavement + drainage + cost_of_land) * gross_tgs * area * area_factor + mobilisation) # - opex empty_stack.maintenance = int( (pavement + drainage) * gross_tgs * area * area_factor * empty_stack.maintenance_perc) # apply proper timing for the crane to come online # stack comes online in year + delivery time, or the same year as the last quay wall (whichever is largest) years_online = [element.year_online for element in core.find_elements(self, Quay_wall)] empty_stack.year_online = max([year + empty_stack.delivery_time, max(years_online)]) # add cash flow information to quay_wall object in a dataframe empty_stack = core.add_cashflow_data_to_element(self, empty_stack) self.elements.append(empty_stack) empty_capacity_planned, empty_capacity_required = self.empty_stack_capacity(year)
[docs] def empty_stack_capacity(self, year): """Calculate the stack capacity for empty containers""" # find the total stack capacity list_of_elements = core.find_elements(self, Empty_Stack) empty_capacity_planned = 0 empty_capacity_online = 0 for element in list_of_elements: empty_capacity_planned += element.capacity if year >= element.year_online: empty_capacity_online += element.capacity # determine the on-terminal total TEU/year for every throughput type (types: ladens, reefers, empties, oogs) laden_teu, reefer_teu, empty_teu, oog_teu = self.throughput_characteristics(year) # Transhipment containers are counted twice in berth throughput calculations – once off the ship and once on the # ship – but are counted only once in the yard capacity calculations. PIANC (2014b), p 63 # total positions = half of the amount that it transhipped + the full amount of what is not transhipped ts = self.transhipment_ratio empty_teu = (empty_teu * ts * 0.5) + (empty_teu * (1 - ts)) # instantiate laden, reefer and stack objects empty = Container(**container_defaults.empty_container_data) stack = Empty_Stack(**container_defaults.empty_stack_data) # calculate operational days operational_days = self.operational_hours / 24 # determine empty ground slots (see Quist and Wijdeven (2014) p. 49) # throughput demand (corrected for ts) x peak factor x dwell times = total nr of containers to be stacked # total nr of containers to be stacked divided by stack height times nr of operational days times stack # occupancy increases nr of containers to be stacked and leads to the number of ground slots you need empty_ground_slots = ((empty_teu * empty.peak_factor * empty.dwell_time) /\ (stack.height * empty.stack_occupancy * operational_days)) empty_capacity_required = empty_ground_slots * stack.height return empty_capacity_planned, empty_capacity_required
[docs] def oog_stack_invest(self, year): """Current strategy is to add stacks as soon as trigger is achieved - find out how much stack capacity is planned - find out how much stack capacity is needed - add stack capacity until service_trigger is no longer exceeded """ oog_capacity_planned, oog_capacity_required = self.oog_stack_capacity(year) if self.debug: print(' OOG slots planned (@ start of year): {:.2f}'.format(oog_capacity_planned)) print(' OOG slots required (@ start of year): {:.2f}'.format(oog_capacity_required)) while oog_capacity_required > oog_capacity_planned: if self.debug: print(' *** add OOG stack to elements') oog_stack = OOG_Stack(**container_defaults.oog_stack_data) # - capex area = oog_stack.length * oog_stack.width gross_tgs = oog_stack.gross_tgs pavement = oog_stack.pavement drainage = oog_stack.drainage area_factor = oog_stack.area_factor mobilisation = oog_stack.mobilisation cost_of_land = self.land_price oog_stack.capex = int((pavement + drainage + cost_of_land) * gross_tgs * area * area_factor + mobilisation) # - opex oog_stack.maintenance = int( (pavement + drainage) * gross_tgs * area * area_factor * oog_stack.maintenance_perc) # - land use stack_ground_slots = oog_stack.capacity / oog_stack.height oog_stack.land_use = stack_ground_slots * oog_stack.gross_tgs # apply proper timing for the crane to come online # stack comes online in year + delivery time, or the same year as the last quay wall (whichever is largest) years_online = [element.year_online for element in core.find_elements(self, Quay_wall)] oog_stack.year_online = max([year + oog_stack.delivery_time, max(years_online)]) # add cash flow information to quay_wall object in a dataframe oog_stack = core.add_cashflow_data_to_element(self, oog_stack) self.elements.append(oog_stack) oog_capacity_planned, oog_capacity_required = self.oog_stack_capacity(year)
[docs] def oog_stack_capacity(self, year): """Calculate the stack capacity for OOG containers""" # find the total stack capacity list_of_elements = core.find_elements(self, OOG_Stack) oog_capacity_planned = 0 oog_capacity_online = 0 for element in list_of_elements: oog_capacity_planned += element.capacity if year >= element.year_online: oog_capacity_online += element.capacity # determine the on-terminal total TEU/year for every throughput type (types: ladens, reefers, empties, oogs) laden_teu, reefer_teu, empty_teu, oog_teu = self.throughput_characteristics(year) # Transhipment containers are counted twice in berth throughput calculations – once off the ship and once on the # ship – but are counted only once in the yard capacity calculations. PIANC (2014b), p 63 # total positions = half of the amount that it transhipped + the full amount of what is not transhipped ts = self.transhipment_ratio oog_teu = (oog_teu * ts * 0.5) + (oog_teu * (1 - ts)) # instantiate laden, reefer and stack objects oog = Container(**container_defaults.oog_container_data) stack = OOG_Stack(**container_defaults.oog_stack_data) # calculate operational days operational_days = self.operational_hours / 24 # determine oog ground slots (see Quist and Wijdeven (2014) p. 49) # throughput demand (corrected for ts) x peak factor x dwell times = total nr of containers to be stacked # total nr of containers to be stacked divided by stack height times nr of operational days times stack # occupancy increases nr of containers to be stacked and leads to the number of ground slots you need oog_ground_spots = ((oog_teu * oog.peak_factor * oog.dwell_time) / (stack.height * oog.stack_occupancy * operational_days * oog.teu_factor)) oog_capacity_required = oog_ground_spots return oog_capacity_planned, oog_capacity_required
[docs] def stack_equipment_invest(self, year): """current strategy is to add stack equipment as soon as a service trigger is achieved - find out how much stack equipment is online - find out how much stack equipment is planned - find out how much stack equipment is needed - add equipment until service_trigger is no longer exceeded """ sts_cranes_online = 0 sts_cranes_planned = 0 stack_equipment_online = 0 stack_equipment_planned = 0 stacks_online = 0 stacks_planned = 0 for element in self.elements: if isinstance(element, Cyclic_Unloader): sts_cranes_planned += 1 if year >= element.year_online: sts_cranes_online += 1 if isinstance(element, Stack_Equipment): stack_equipment_planned += 1 if year >= element.year_online: stack_equipment_online += 1 if isinstance(element, Laden_Stack): stacks_planned += 1 if year >= element.year_online: stacks_online += 1 if self.stack_equipment == 'rtg': stack_equipment = Stack_Equipment(**container_defaults.rtg_data) elif self.stack_equipment == 'rmg': stack_equipment = Stack_Equipment(**container_defaults.rmg_data) elif self.stack_equipment == 'sc': stack_equipment = Stack_Equipment(**container_defaults.sc_data) elif self.stack_equipment == 'rs': stack_equipment = Stack_Equipment(**container_defaults.rs_data) if self.debug: print(' Number of stack equipment online (@ start of year): {}'.format(stack_equipment_online)) # the rtg, sc and rs are coupled with the STS cranes, the rmg with the stack if (self.stack_equipment == 'rtg' or self.stack_equipment == 'sc' or self.stack_equipment == 'rs'): governing_object = sts_cranes_planned elif self.stack_equipment == 'rmg': governing_object = stacks_planned while governing_object * stack_equipment.required > stack_equipment_planned: # add stack equipment when not enough to serve number of STS cranes if self.debug: print(' *** add stack equipment to elements') # - capex unit_rate = stack_equipment.unit_rate mobilisation = stack_equipment.mobilisation stack_equipment.capex = int(unit_rate + mobilisation) # - opex # todo calculate moves for energy costs stack_equipment.insurance = unit_rate * stack_equipment.insurance_perc stack_equipment.maintenance = unit_rate * stack_equipment.maintenance_perc # labour labour = Labour(**container_defaults.labour_data) stack_equipment.shift = stack_equipment.crew * labour.daily_shifts stack_equipment.labour = stack_equipment.shift * labour.blue_collar_salary # apply proper timing for the crane to come online # year + delivery time or in the year as the last laden stack years_online = [] for element in core.find_elements(self, Laden_Stack): years_online.append(element.year_online) stack_equipment.year_online = max([year + stack_equipment.delivery_time, max(years_online)]) # add cash flow information to tractor object in a dataframe stack_equipment = core.add_cashflow_data_to_element(self, stack_equipment) self.elements.append(stack_equipment) # add one to planned stack equipment (important for while loop) stack_equipment_planned += 1 if self.debug: print(' a total of {} stack equipment is online; {} stack equipment still pending'.format( stack_equipment_online, stack_equipment_planned - stack_equipment_online))
[docs] def empty_handler_invest(self, year): """current strategy is to add empty hanlders as soon as a service trigger is achieved - find out how many empty handlers are online - find out how many empty handlers areplanned - find out how many empty handlers are needed - add empty handlers until service_trigger is no longer exceeded """ sts_cranes_planned = len(core.find_elements(self, Cyclic_Unloader)) empty_handlers_planned = 0 empty_handlers_online = 0 for element in self.elements: if isinstance(element, Empty_Handler): empty_handlers_planned += 1 if year >= element.year_online: empty_handlers_online += 1 if self.debug: print(' Empty handlers planned (@ start of year): {}'.format(empty_handlers_planned)) # object needs to be instantiated here so that empty_handler.required may be determined empty_handler = Empty_Handler(**container_defaults.empty_handler_data) while sts_cranes_planned * empty_handler.required > empty_handlers_planned: # add a tractor when not enough to serve number of STS cranes if self.debug: print(' *** add empty handler to elements') # - capex unit_rate = empty_handler.unit_rate mobilisation = empty_handler.mobilisation empty_handler.capex = int(unit_rate + mobilisation) # - opex empty_handler.maintenance = unit_rate * empty_handler.maintenance_perc # labour labour = Labour(**container_defaults.labour_data) empty_handler.shift = empty_handler.crew * labour.daily_shifts empty_handler.labour = empty_handler.shift * labour.blue_collar_salary # apply proper timing for the empty handler to come online # year + empty_handler.delivery_time or last Empty_Stack, which ever is largest years_online = [] for element in core.find_elements(self, Empty_Stack): years_online.append(element.year_online) empty_handler.year_online = max([year + empty_handler.delivery_time, max(years_online)]) # add cash flow information to tractor object in a dataframe empty_handler = core.add_cashflow_data_to_element(self, empty_handler) self.elements.append(empty_handler) empty_handlers_planned += 1 if self.debug: print(' a total of {} empty handlers is online; {} empty handlers still pending'.format( empty_handlers_online, empty_handlers_planned - empty_handlers_online))
[docs] def gate_invest(self, year): """current strategy is to add gates as soon as trigger is achieved - find out how much gate capacity is online - find out how much gate capacity is planned - find out how much gate capacity is needed - add gate capacity until service_trigger is no longer exceeded """ gate_capacity_planned, gate_capacity_online, service_rate_planned, total_design_gate_minutes = \ self.calculate_gate_minutes(year) if self.debug: print(' Gate capacity online (@ start of year): {:.2f}'.format(gate_capacity_online)) print(' Gate capacity planned (@ start of year): {:.2f}'.format(gate_capacity_planned)) print(' Service rate planned (@ start of year): {:.2f}'.format(service_rate_planned)) print(' Gate lane minutes (@ start of year): {:.2f}'.format(total_design_gate_minutes)) while service_rate_planned > 1: if self.debug: print(' *** add gate to elements') gate = Gate(**container_defaults.gate_data) # - land use gate.land_use = gate.area # - capex unit_rate = gate.unit_rate mobilisation = gate.mobilisation canopy = gate.canopy_costs * gate.area cost_of_land = self.land_price gate.capex = int(unit_rate + mobilisation + canopy + (cost_of_land * gate.area)) # - opex gate.maintenance = unit_rate * gate.maintenance_perc # labour labour = Labour(**container_defaults.labour_data) gate.shift = gate.crew * labour.daily_shifts gate.labour = gate.shift * labour.blue_collar_salary if year == self.startyear: gate.year_online = year + gate.delivery_time + 1 else: gate.year_online = year + gate.delivery_time # add cash flow information to tractor object in a dataframe gate = core.add_cashflow_data_to_element(self, gate) self.elements.append(gate) gate_capacity_planned, gate_capacity_online, service_rate_planned, total_design_gate_minutes = \ self.calculate_gate_minutes(year)
[docs] def calculate_gate_minutes(self, year): """ - Find all gates and sum their effective_capacity to get service_capacity - Calculate average entry and exit time to get total time at gate - Occupancy is total_minutes_at_gate per hour divided by 1 hour """ # find the online and planned Gate capacity list_of_elements = core.find_elements(self, Gate) gate_capacity_planned = 0 gate_capacity_online = 0 total_design_gate_minutes = 0 if list_of_elements != []: for element in list_of_elements: gate_capacity_planned += element.capacity if year >= element.year_online: gate_capacity_online += element.capacity # estimate time at gate lanes '''Get input: import box moves en export box moves, translate to design gate lanes per hour. Every gate is 60 minutes, which is the capacity. Dan is het gewoon while totaal is meer dan totale capacity gate toevoegen''' ''' Calculate the total throughput in TEU per year''' laden_box, reefer_box, empty_box, oog_box, throughput_box = self.throughput_box(year) # half of the transhipment moves is import and half is export import_box_moves = (throughput_box * (1 - self.transhipment_ratio)) * 0.5 # assume import / export is always 50/50 export_box_moves = (throughput_box * (1 - self.transhipment_ratio)) * 0.5 # assume import / export is always 50/50 weeks_year = 52 # instantiate a Gate object gate = Gate(**container_defaults.gate_data) # Todo: this really needs to be checked design_exit_gate_minutes = import_box_moves * (gate.truck_moves / weeks_year) * gate.peak_factor * \ gate.peak_day * gate.peak_hour * \ gate.exit_inspection_time * gate.design_capacity # Todo: this really needs to be checked design_entry_gate_minutes = export_box_moves * (gate.truck_moves / weeks_year) * gate.peak_factor * \ gate.peak_day * gate.peak_hour * \ gate.entry_inspection_time * gate.design_capacity total_design_gate_minutes = design_entry_gate_minutes + design_exit_gate_minutes service_rate_planned = total_design_gate_minutes / gate_capacity_planned else: service_rate_planned = float("inf") return gate_capacity_planned, gate_capacity_online, service_rate_planned, total_design_gate_minutes
[docs] def general_services_invest(self, year): laden_teu, reefer_teu, empty_teu, oog_teu = self.throughput_characteristics(year) throughput = laden_teu + reefer_teu + oog_teu + empty_teu cranes = 0 general = 0 for element in self.elements: if isinstance(element, Cyclic_Unloader): if year >= element.year_online: cranes += 1 if isinstance(element, General_Services): if year >= element.year_online: general += 1 sts_cranes = cranes general = General_Services(**container_defaults.general_services_data) quay_land_use = 0 stack_land_use = 0 empty_land_use = 0 oog_land_use = 0 gate_land_use = 0 for element in self.elements: if isinstance(element, Quay_wall): if year >= element.year_online: quay_land_use += element.land_use if isinstance(element, Laden_Stack): if year >= element.year_online: stack_land_use += element.land_use if isinstance(element, Empty_Stack): if year >= element.year_online: empty_land_use += element.land_use if isinstance(element, OOG_Stack): if year >= element.year_online: oog_land_use += element.land_use if isinstance(element, Gate): if year >= element.year_online: gate_land_use += element.land_use total_land_use = \ (quay_land_use + stack_land_use + empty_land_use + oog_land_use + gate_land_use + general.office + general.workshop + general.scanning_inspection_area + general.repair_building) * 0.0001 if year == (self.startyear + 1): # add general services as soon as berth is online if self.debug: print(' *** add general services to elements') # land use general.land_use = general.office + general.workshop + general.scanning_inspection_area \ + general.repair_building # - capex area = general.office + general.workshop + general.scanning_inspection_area \ + general.repair_building cost_of_land = self.land_price office = general.office * general.office_cost workshop = general.workshop * general.workshop_cost inspection = general.scanning_inspection_area * general.scanning_inspection_area_cost light = general.lighting_mast_cost * (total_land_use / general.lighting_mast_required) repair = general.repair_building * general.repair_building_cost basic = general.fuel_station_cost + general.firefight_cost + general.maintenance_tools_cost \ + general.terminal_operating_software_cost + general.electrical_station_cost general.capex = office + workshop + inspection + light + repair + basic + (area * cost_of_land) # - opex general.maintenance = general.capex * general.general_maintenance if year == self.startyear: general.year_online = year + general.delivery_time else: general.year_online = year + general.delivery_time # add cash flow information to tractor object in a dataframe general = core.add_cashflow_data_to_element(self, general) self.elements.append(general)
# Todo: include CFS (container freight station) # *** Various cost calculation methods
[docs] def calculate_energy_cost(self, year): """ # todo voeg energy toe voor nieuwe elementen """ sts_moves, tractor_moves, empty_moves, stack_moves = self.box_moves(year) energy_price = self.energy_price # STS crane energy costs cranes = 0 for element in self.elements: if isinstance(element, Cyclic_Unloader): if year >= element.year_online: cranes += 1 for element in core.find_elements(self, Cyclic_Unloader): if year >= element.year_online: sts_moves_per_element = sts_moves / cranes if element.consumption * sts_moves_per_element * energy_price != np.inf: element.df.loc[element.df['year'] == year, 'energy'] = \ element.consumption * sts_moves_per_element * energy_price else: element.df.loc[element.df['year'] == year, 'energy'] = 0 # calculate stack equipment energy costs if self.stack_equipment == 'rmg': list_of_elements_Stack = core.find_elements(self, Stack_Equipment) equipment = 0 for element in self.elements: if isinstance(element, Stack_Equipment): if year >= element.year_online: equipment += 1 for element in list_of_elements_Stack: if year >= element.year_online: moves = stack_moves / equipment consumption = element.power_consumption costs = energy_price if consumption * costs * moves != np.inf: element.df.loc[element.df['year'] == year, 'energy'] = consumption * costs * moves else: element.df.loc[element.df['year'] == year, 'energy'] = 0 # reefer energy costs stack_capacity_planned, total_capacity_required, reefer_ground_slots = self.laden_reefer_stack_capacity(year) stacks = 0 for element in self.elements: if isinstance(element, Laden_Stack): if year >= element.year_online: stacks += 1 for element in core.find_elements(self, Laden_Stack): if year >= element.year_online: slots_per_stack = reefer_ground_slots / stacks if slots_per_stack * element.reefers_present * energy_price * 24 * 365 != np.inf: element.df.loc[element.df['year'] == year, 'energy'] = slots_per_stack * element.reefers_present \ * energy_price * 24 * 365 else: element.df.loc[element.df['year'] == year, 'energy'] = 0 # Calculate general power use general = General_Services(**container_defaults.general_services_data) # - lighting quay_land_use = 0 stack_land_use = 0 empty_land_use = 0 oog_land_use = 0 gate_land_use = 0 general_land_use = 0 for element in self.elements: if isinstance(element, Quay_wall): if year >= element.year_online: quay_land_use += element.land_use if isinstance(element, Laden_Stack): if year >= element.year_online: stack_land_use += element.land_use if isinstance(element, Empty_Stack): if year >= element.year_online: empty_land_use += element.land_use if isinstance(element, OOG_Stack): if year >= element.year_online: oog_land_use += element.land_use if isinstance(element, Gate): if year >= element.year_online: gate_land_use += element.land_use if isinstance(element, General_Services): if year >= element.year_online: general_land_use += element.land_use total_land_use = quay_land_use + stack_land_use + empty_land_use + oog_land_use + gate_land_use + general_land_use lighting = total_land_use * energy_price * general.lighting_consumption # - office, gates, workshops power use general_consumption = general.general_consumption * energy_price * self.operational_hours for element in core.find_elements(self, General_Services): if year >= element.year_online: if lighting + general_consumption != np.inf: element.df.loc[element.df['year'] == year, 'energy'] = lighting + general_consumption else: element.df.loc[element.df['year'] == year, 'energy'] = 0
[docs] def calculate_general_labour_cost(self, year): """General labour""" general = General_Services(**container_defaults.general_services_data) laden_teu, reefer_teu, empty_teu, oog_teu = self.throughput_characteristics(year) throughput = laden_teu + reefer_teu + oog_teu + empty_teu labour = Labour(**container_defaults.labour_data) cranes = 0 for element in self.elements: if isinstance(element, Cyclic_Unloader): if year >= element.year_online: cranes += 1 sts_cranes = cranes if sts_cranes != 0: crew_required = np.ceil(throughput / general.crew_required) # fixed labour total_fte_fixed = crew_required * ( general.ceo + general.secretary + general.administration + general.hr + general.commercial) fixed_labour = total_fte_fixed * labour.white_collar_salary # shift labour white_collar = crew_required * labour.daily_shifts * (general.operations) * labour.white_collar_salary blue_collar = crew_required * labour.daily_shifts * ( general.engineering + general.security) * labour.blue_collar_salary shift_labour = white_collar + blue_collar # total labour list_of_elements_general = core.find_elements(self, General_Services) for element in list_of_elements_general: if year >= element.year_online: if fixed_labour + shift_labour != np.inf: element.df.loc[element.df['year'] == year, 'labour'] = fixed_labour + shift_labour else: element.df.loc[element.df['year'] == year, 'labour'] = 0
[docs] def calculate_fuel_cost(self, year): """Fuel cost""" sts_moves, tractor_moves, empty_moves, stack_moves = self.box_moves(year) fuel_price = self.fuel_price # calculate empty handler fuel costs list_of_elements_ech = core.find_elements(self, Empty_Handler) equipment = 0 for element in self.elements: if isinstance(element, Empty_Handler): if year >= element.year_online: equipment += 1 for element in list_of_elements_ech: if year >= element.year_online: moves = empty_moves / equipment consumption = element.fuel_consumption costs = fuel_price if consumption * costs * moves != np.inf: element.df.loc[element.df['year'] == year, 'fuel'] = consumption * costs * moves else: element.df.loc[element.df['year'] == year, 'fuel'] = 0 # calculate stack equipment fuel costs if self.stack_equipment == 'rtg' or self.stack_equipment == 'rs' or self.stack_equipment == 'sc': list_of_elements_Stack = core.find_elements(self, Stack_Equipment) equipment = 0 for element in self.elements: if isinstance(element, Stack_Equipment): if year >= element.year_online: equipment += 1 for element in list_of_elements_Stack: if year >= element.year_online: moves = stack_moves / equipment consumption = element.fuel_consumption costs = fuel_price if consumption * costs * moves != np.inf: element.df.loc[element.df['year'] == year, 'fuel'] = consumption * costs * moves else: element.df.loc[element.df['year'] == year, 'fuel'] = 0 # calculate tractor fuel consumption list_of_elements_Tractor = core.find_elements(self, Horizontal_Transport) transport = 0 for element in self.elements: if isinstance(element, Horizontal_Transport): if year >= element.year_online: transport += 1 for element in list_of_elements_Tractor: if year >= element.year_online: moves = tractor_moves / transport if element.fuel_consumption * moves * fuel_price != np.inf: element.df.loc[element.df['year'] == year, 'fuel'] = \ element.fuel_consumption * moves * fuel_price else: element.df.loc[element.df['year'] == year, 'fuel'] = 0
[docs] def calculate_demurrage_cost(self, year): """Find the demurrage cost per type of vessel and sum all demurrage cost""" fully_cellular_calls, panamax_calls, panamax_max_calls, post_panamax_I_calls, post_panamax_II_calls, \ new_panamax_calls, VLCS_calls, ULCS_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, fully_cellular_calls, panamax_calls, panamax_max_calls, post_panamax_I_calls, post_panamax_II_calls, new_panamax_calls, VLCS_calls, ULCS_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)): if year >= element.year_online: service_rate += element.effective_capacity / quay_walls # Find the demurrage cost per type of vessel if service_rate != 0: fully_cellular = Vessel(**container_defaults.fully_cellular_data) service_time_fully_cellular = fully_cellular.call_size / service_rate waiting_time_hours_fully_cellular = waiting_factor * service_time_fully_cellular port_time_fully_cellular = waiting_time_hours_fully_cellular + service_time_fully_cellular + fully_cellular.mooring_time penalty_time_fully_cellular = max(0, port_time_fully_cellular - fully_cellular.all_turn_time) demurrage_time_fully_cellular = penalty_time_fully_cellular * fully_cellular_calls demurrage_cost_fully_cellular = demurrage_time_fully_cellular * fully_cellular.demurrage_rate panamax = Vessel(**container_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 panamax_max = Vessel(**container_defaults.panamax_max_data) service_time_panamax_max = panamax_max.call_size / service_rate waiting_time_hours_panamax_max = waiting_factor * service_time_panamax_max port_time_panamax_max = waiting_time_hours_panamax_max + service_time_panamax_max + panamax_max.mooring_time penalty_time_panamax_max = max(0, port_time_panamax_max - panamax_max.all_turn_time) demurrage_time_panamax_max = penalty_time_panamax_max * panamax_max_calls demurrage_cost_panamax_max = demurrage_time_panamax_max * panamax_max.demurrage_rate post_panamax_I = Vessel(**container_defaults.post_panamax_I_data) service_time_post_panamax_I = post_panamax_I.call_size / service_rate waiting_time_hours_post_panamax_I = waiting_factor * service_time_post_panamax_I port_time_post_panamax_I = waiting_time_hours_post_panamax_I + service_time_post_panamax_I + post_panamax_I.mooring_time penalty_time_post_panamax_I = max(0, port_time_post_panamax_I - post_panamax_I.all_turn_time) demurrage_time_post_panamax_I = penalty_time_post_panamax_I * post_panamax_I_calls demurrage_cost_post_panamax_I = demurrage_time_post_panamax_I * post_panamax_I.demurrage_rate post_panamax_II = Vessel(**container_defaults.post_panamax_II_data) service_time_post_panamax_II = post_panamax_II.call_size / service_rate waiting_time_hours_post_panamax_II = waiting_factor * service_time_post_panamax_II port_time_post_panamax_II = waiting_time_hours_post_panamax_II + service_time_post_panamax_II + post_panamax_II.mooring_time penalty_time_post_panamax_II = max(0, port_time_post_panamax_II - post_panamax_II.all_turn_time) demurrage_time_post_panamax_II = penalty_time_post_panamax_II * post_panamax_II_calls demurrage_cost_post_panamax_II = demurrage_time_post_panamax_II * post_panamax_II.demurrage_rate new_panamax = Vessel(**container_defaults.new_panamax_data) service_time_new_panamax = new_panamax.call_size / service_rate waiting_time_hours_new_panamax = waiting_factor * service_time_new_panamax port_time_new_panamax = waiting_time_hours_new_panamax + service_time_new_panamax + new_panamax.mooring_time penalty_time_new_panamax = max(0, port_time_new_panamax - new_panamax.all_turn_time) demurrage_time_new_panamax = penalty_time_new_panamax * new_panamax_calls demurrage_cost_new_panamax = demurrage_time_new_panamax * new_panamax.demurrage_rate VLCS = Vessel(**container_defaults.VLCS_data) service_time_VLCS = VLCS.call_size / service_rate waiting_time_hours_VLCS = waiting_factor * service_time_VLCS port_time_VLCS = waiting_time_hours_VLCS + service_time_VLCS + VLCS.mooring_time penalty_time_VLCS = max(0, port_time_VLCS - VLCS.all_turn_time) demurrage_time_VLCS = penalty_time_VLCS * VLCS_calls demurrage_cost_VLCS = demurrage_time_VLCS * VLCS.demurrage_rate ULCS = Vessel(**container_defaults.ULCS_data) service_time_ULCS = ULCS.call_size / service_rate waiting_time_hours_ULCS = waiting_factor * service_time_ULCS port_time_ULCS = waiting_time_hours_ULCS + service_time_ULCS + ULCS.mooring_time penalty_time_ULCS = max(0, port_time_ULCS - ULCS.all_turn_time) demurrage_time_ULCS = penalty_time_ULCS * ULCS_calls demurrage_cost_ULCS = demurrage_time_ULCS * ULCS.demurrage_rate else: demurrage_cost_fully_cellular = 0 demurrage_cost_panamax = 0 demurrage_cost_panamax_max = 0 demurrage_cost_post_panamax_I = 0 demurrage_cost_post_panamax_II = 0 demurrage_cost_new_panamax = 0 demurrage_cost_VLCS = 0 demurrage_cost_ULCS = 0 total_demurrage_cost = demurrage_cost_fully_cellular + demurrage_cost_panamax + demurrage_cost_panamax_max + \ demurrage_cost_post_panamax_I + demurrage_cost_post_panamax_II + \ demurrage_cost_new_panamax + demurrage_cost_VLCS + demurrage_cost_ULCS self.demurrage.append(total_demurrage_cost)
[docs] def calculate_indirect_costs(self): """Indirect costs are a function of overall CAPEX.""" # Todo: check why this is not done per year indirect = Indirect_Costs(**container_defaults.indirect_costs_data) # collect CAPEX from terminal elements cash_flows, cash_flows_WACC_real = core.add_cashflow_elements(self, Labour(**container_defaults.labour_data)) capex = cash_flows['capex'].values # add indirect costs for different stack equipment: # - electrical work, miscellaneous, preliminaries and engineering if self.stack_equipment == 'rtg' or self.stack_equipment == 'rs' or self.stack_equipment == 'sc': electrical_works = indirect.electrical_works_fuel_terminal * capex elif self.stack_equipment == 'rmg' or self.stack_equipment == 'ertg': electrical_works = indirect.electrical_works_power_terminal * capex miscellaneous = indirect.miscellaneous * capex preliminaries = indirect.preliminaries * capex engineering = indirect.engineering * capex indirect_costs = capex + electrical_works + miscellaneous + preliminaries + engineering print(indirect_costs) cash_flows['capex'].values = indirect_costs
# *** General functions
[docs] def calculate_vessel_calls(self, year): """Calculate volumes to be transported and the number of vessel calls (both per vessel type and in total) """ # intialize values to be returned fully_cellular_vol = 0 panamax_vol = 0 panamax_max_vol = 0 post_panamax_I_vol = 0 post_panamax_II_vol = 0 new_panamax_vol = 0 VLCS_vol = 0 ULCS_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() # The total amount of annualy transported TEU fully_cellular_vol += volume * commodity.fully_cellular_perc / 100 panamax_vol += volume * commodity.panamax_perc / 100 panamax_max_vol += volume * commodity.panamax_max_perc / 100 post_panamax_I_vol += volume * commodity.post_panamax_I_perc / 100 post_panamax_II_vol += volume * commodity.post_panamax_II_perc / 100 new_panamax_vol += volume * commodity.new_panamax_perc / 100 VLCS_vol += volume * commodity.VLCS_perc / 100 ULCS_vol += volume * commodity.ULCS_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 == 'Fully_Cellular': fully_cellular_calls = int(np.ceil(fully_cellular_vol / vessel.call_size)) elif vessel.type == 'Panamax': panamax_calls = int(np.ceil(panamax_vol / vessel.call_size)) elif vessel.type == 'Panamax_Max': panamax_max_calls = int(np.ceil(panamax_max_vol / vessel.call_size)) elif vessel.type == 'Post_Panamax_I': post_panamax_I_calls = int(np.ceil(post_panamax_I_vol / vessel.call_size)) elif vessel.type == 'Post_Panamax_II': post_panamax_II_calls = int(np.ceil(post_panamax_II_vol / vessel.call_size)) elif vessel.type == 'New_Panamax': new_panamax_calls = int(np.ceil(new_panamax_vol / vessel.call_size)) elif vessel.type == 'VLCS': VLCS_calls = int(np.ceil(VLCS_vol / vessel.call_size)) elif vessel.type == 'ULCS': ULCS_calls = int(np.ceil(ULCS_vol / vessel.call_size)) total_calls = np.sum([fully_cellular_calls, panamax_calls, panamax_max_calls, post_panamax_I_calls, post_panamax_II_calls, new_panamax_calls, VLCS_calls, ULCS_calls]) return fully_cellular_calls, panamax_calls, panamax_max_calls, \ post_panamax_I_calls, post_panamax_II_calls, new_panamax_calls, \ VLCS_calls, ULCS_calls, \ total_calls, total_vol
[docs] def calculate_berth_occupancy(self, year, fully_cellular_calls, panamax_calls, panamax_max_calls, post_panamax_I_calls, post_panamax_II_calls, new_panamax_calls, VLCS_calls, ULCS_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 the system list_of_elements_cranes = core.find_elements(self, Cyclic_Unloader) list_of_elements_berths = core.find_elements(self, Berth) # Todo: check if nr_berths is important to include or nor nr_berths = len(list_of_elements_berths) # 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_cranes != []: for element in list_of_elements_cranes: service_rate_planned += element.effective_capacity if year >= element.year_online: service_rate_online += element.effective_capacity time_mooring_unmooring_fully_cellular = fully_cellular_calls * \ container_defaults.fully_cellular_data["mooring_time"] time_at_cranes_planned_fully_cellular = fully_cellular_calls * \ (container_defaults.fully_cellular_data["call_size"] / service_rate_planned) time_mooring_unmooring_panamax = panamax_calls * \ container_defaults.panamax_data["mooring_time"] time_at_cranes_planned_panamax = panamax_calls * \ (container_defaults.panamax_data["call_size"] / service_rate_planned) time_mooring_unmooring_panamax_max = panamax_max_calls * \ container_defaults.panamax_max_data["mooring_time"] time_at_cranes_planned_panamax_max = panamax_max_calls * \ (container_defaults.panamax_max_data["call_size"] / service_rate_planned) time_mooring_unmooring_post_panamax_I = post_panamax_I_calls * \ container_defaults.post_panamax_I_data["mooring_time"] time_at_cranes_planned_post_panamax_I = post_panamax_I_calls * \ (container_defaults.post_panamax_I_data["call_size"] / service_rate_planned) time_mooring_unmooring_post_panamax_II = post_panamax_II_calls * \ container_defaults.post_panamax_II_data["mooring_time"] time_at_cranes_planned_post_panamax_II = post_panamax_II_calls * \ (container_defaults.post_panamax_II_data["call_size"] / service_rate_planned) time_mooring_unmooring_new_panamax = new_panamax_calls * \ container_defaults.new_panamax_data["mooring_time"] time_at_cranes_planned_new_panamax = new_panamax_calls * \ (container_defaults.new_panamax_data["call_size"] / service_rate_planned) time_mooring_unmooring_VLCS = VLCS_calls * \ container_defaults.VLCS_data["mooring_time"] time_at_cranes_planned_VLCS = VLCS_calls * \ (container_defaults.VLCS_data["call_size"] / service_rate_planned) time_mooring_unmooring_ULCS = ULCS_calls * \ container_defaults.ULCS_data["mooring_time"] time_at_cranes_planned_ULCS = ULCS_calls * \ (container_defaults.ULCS_data["call_size"] / service_rate_planned) # total time at berth total_time_at_berth_planned = np.sum( [time_mooring_unmooring_fully_cellular + time_at_cranes_planned_fully_cellular, time_mooring_unmooring_panamax + time_at_cranes_planned_panamax, time_mooring_unmooring_panamax_max + time_at_cranes_planned_panamax_max, time_mooring_unmooring_post_panamax_I + time_at_cranes_planned_post_panamax_I, time_mooring_unmooring_post_panamax_II + time_at_cranes_planned_post_panamax_II, time_mooring_unmooring_new_panamax + time_at_cranes_planned_new_panamax, time_mooring_unmooring_VLCS + time_at_cranes_planned_VLCS, time_mooring_unmooring_ULCS + time_at_cranes_planned_ULCS]) # total time at cranes total_time_at_cranes_planned = np.sum( [time_at_cranes_planned_fully_cellular, time_at_cranes_planned_panamax, time_at_cranes_planned_panamax_max, time_at_cranes_planned_post_panamax_I, time_at_cranes_planned_post_panamax_II, time_at_cranes_planned_new_panamax, time_at_cranes_planned_VLCS, time_at_cranes_planned_ULCS]) # 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 = total_time_at_cranes_planned / self.operational_hours if service_rate_online != 0: # when some cranes are actually online time_at_cranes_online_fully_cellular = fully_cellular_calls * \ (container_defaults.fully_cellular_data["call_size"] / service_rate_online) time_at_cranes_online_panamax = panamax_calls * \ (container_defaults.panamax_data["call_size"] / service_rate_online) time_at_cranes_online_panamax_max = panamax_max_calls * \ (container_defaults.panamax_max_data["call_size"] / service_rate_online) time_at_cranes_online_post_panamax_I = post_panamax_I_calls * \ (container_defaults.post_panamax_I_data["call_size"] / service_rate_online) time_at_cranes_online_post_panamax_II = post_panamax_II_calls * \ (container_defaults.post_panamax_II_data["call_size"] / service_rate_online) time_at_cranes_online_new_panamax = new_panamax_calls * \ (container_defaults.new_panamax_data["call_size"] / service_rate_online) time_at_cranes_online_VLCS = VLCS_calls * \ (container_defaults.VLCS_data["call_size"] / service_rate_online) time_at_cranes_online_ULCS = ULCS_calls * \ (container_defaults.ULCS_data["call_size"] / service_rate_online) total_time_at_berth_online = np.sum( [time_mooring_unmooring_fully_cellular + time_at_cranes_online_fully_cellular, time_mooring_unmooring_panamax + time_at_cranes_online_panamax, time_mooring_unmooring_panamax_max + time_at_cranes_online_panamax_max, time_mooring_unmooring_post_panamax_I + time_at_cranes_online_post_panamax_I, time_mooring_unmooring_post_panamax_II + time_at_cranes_online_post_panamax_II, time_mooring_unmooring_new_panamax + time_at_cranes_online_new_panamax, time_mooring_unmooring_VLCS + time_at_cranes_online_VLCS, time_mooring_unmooring_ULCS + time_at_cranes_online_ULCS]) total_time_at_cranes_online = np.sum( [time_at_cranes_online_fully_cellular, time_at_cranes_online_panamax, time_at_cranes_online_panamax_max, time_at_cranes_online_post_panamax_I, time_at_cranes_online_post_panamax_II, time_at_cranes_online_new_panamax, time_at_cranes_online_VLCS, time_at_cranes_online_ULCS]) # 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([total_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 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 = core.find_elements(self, Cyclic_Unloader) # 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 calculate_throughput(self, year): """Find throughput (minimum of crane capacity and demand)""" # 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 # find the total service rate and determine the capacity at the quay list_of_elements = core.find_elements(self, Cyclic_Unloader) quay_capacity_planned = 0 quay_capacity_online = 0 if list_of_elements != []: for element in list_of_elements: quay_capacity_planned += ( element.effective_capacity * self.operational_hours * self.allowable_berth_occupancy) if year >= element.year_online: quay_capacity_online += ( element.effective_capacity * self.operational_hours * self.allowable_berth_occupancy) if quay_capacity_online is not 0: throughput_online = min(quay_capacity_online, total_vol) else: throughput_online = total_vol return throughput_online
[docs] def throughput_characteristics(self, year): """ - Find commodity volume - Find the on terminal modal split (types: ladens, empties, reefers, oogs) - Return the total TEU/year for every throughput type """ # Calculate the total throughput in TEU per year commodities = core.find_elements(self, Commodity) for commodity in commodities: try: volume = commodity.scenario_data.loc[commodity.scenario_data['year'] == year]['volume'].item() except: pass # divide throughput over different categories based on indicated split laden_teu = volume * self.laden_perc empty_teu = volume * self.empty_perc reefer_teu = volume * self.reefer_perc oog_teu = volume * self.oog_perc return laden_teu, reefer_teu, empty_teu, oog_teu
[docs] def throughput_box(self, year): """ - Find the total TEU/year for every throughput type (types: ladens, empties, reefers, oogs) - Translate the total TEU/year to number of boxes for every throughput type """ # import container throughput laden_teu, reefer_teu, empty_teu, oog_teu = self.throughput_characteristics(year) # instantiate terminal objexts laden = Container(**container_defaults.laden_container_data) reefer = Container(**container_defaults.reefer_container_data) empty = Container(**container_defaults.empty_container_data) oog = Container(**container_defaults.oog_container_data) laden_box = laden_teu / laden.teu_factor reefer_box = reefer_teu / reefer.teu_factor empty_box = empty_teu / empty.teu_factor oog_box = oog_teu / oog.teu_factor throughput_box = laden_box + reefer_box + empty_box + oog_box return laden_box, reefer_box, empty_box, oog_box, throughput_box
[docs] def box_moves(self, year): """Calculate the box moves as input for the power and fuel consumption""" laden_box, reefer_box, empty_box, oog_box, throughput_box = self.throughput_box(year) # calculate STS moves (equal to the throughput) sts_moves = throughput_box # calculate the number of tractor moves tractor = Horizontal_Transport(**container_defaults.tractor_trailer_data) tractor_moves = throughput_box * tractor.non_essential_moves # calculate the number of empty moves empty = Empty_Stack(**container_defaults.empty_stack_data) empty_moves = empty_box * empty.household * empty.digout # Todo: wellicht reefer and laden nog scheiden van elkaar in alles # calculate laden and reefer stack moves if self.laden_stack == 'rtg': # Rubber Tired Gantry crane stack = Laden_Stack(**container_defaults.rtg_stack_data) elif self.laden_stack == 'rmg': # Rail Mounted Gantry crane stack = Laden_Stack(**container_defaults.rmg_stack_data) elif self.laden_stack == 'sc': # Straddle Carrier stack = Laden_Stack(**container_defaults.sc_stack_data) elif self.laden_stack == 'rs': # Reach Stacker stack = Laden_Stack(**container_defaults.rs_stack_data) # The number of moves per laden box moves for transhipment (t/s) moves_t_s = 0.5 * ((2 + stack.household) * stack.digout_margin) # The number of moves per laden box moves for import and export (i/e) digout_moves = (stack.height - 1) / 2 # JvBeemen moves_i_e = ((2 + stack.household + digout_moves) + ((2 + stack.household) * stack.digout_margin)) / 2 # The number of laden/reefer boxes for transhipment (t/s) laden_reefer_box_t_s = (laden_box + reefer_box) * self.transhipment_ratio # The number of laden/reefer boxes for import and export (i/e) laden_reefer_box_i_e = (laden_box + reefer_box) - laden_reefer_box_t_s # The number of moves for transhipment (t/s) laden_reefer_moves_t_s = laden_reefer_box_t_s * moves_t_s # The number of moves for import and export (i/e) laden_reefer_moves_i_e = laden_reefer_box_i_e * moves_i_e # number of stack moves is import export moves + transhipment moves stack_moves = laden_reefer_moves_i_e + laden_reefer_moves_t_s return sts_moves, tractor_moves, empty_moves, stack_moves
[docs] def calculate_land_use(self, year): """Calculate total land use by summing all land_use values of the physical terminal elements""" quay_land_use = 0 stack_land_use = 0 empty_land_use = 0 oog_land_use = 0 gate_land_use = 0 general_land_use = 0 for element in self.elements: if isinstance(element, Quay_wall): if year >= element.year_online: quay_land_use += element.land_use if isinstance(element, Laden_Stack): if year >= element.year_online: stack_land_use += element.land_use if isinstance(element, Empty_Stack): if year >= element.year_online: empty_land_use += element.land_use if isinstance(element, OOG_Stack): if year >= element.year_online: oog_land_use += element.land_use if isinstance(element, Gate): if year >= element.year_online: gate_land_use += element.land_use if isinstance(element, General_Services): if year >= element.year_online: general_land_use += element.land_use # sum total of all total_land_use = \ quay_land_use + \ stack_land_use + \ empty_land_use + \ oog_land_use + \ gate_land_use + \ general_land_use return total_land_use
# *** Plotting functions
[docs] def terminal_elements_plot(self, width=0.08, alpha=0.6, fontsize=20, demand_step=50_000): """Gather data from Terminal and plot which elements come online when""" years = [] berths = [] quays = [] cranes = [] tractor_trailer = [] laden_reefer_stack = [] empty_stack = [] oog_stack = [] stack_equipment = [] empty_handler = [] gates = [] for year in range(self.startyear, self.startyear + self.lifecycle): years.append(year) berths.append(0) quays.append(0) cranes.append(0) tractor_trailer.append(0) laden_reefer_stack.append(0) empty_stack.append(0) oog_stack.append(0) stack_equipment.append(0) empty_handler.append(0) gates.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): if year >= element.year_online: cranes[-1] += 1 if isinstance(element, Horizontal_Transport): if year >= element.year_online: tractor_trailer[-1] += 1 if isinstance(element, Laden_Stack): if year >= element.year_online: laden_reefer_stack[-1] += 1 if isinstance(element, Empty_Stack): if year >= element.year_online: empty_stack[-1] += 1 if isinstance(element, OOG_Stack): if year >= element.year_online: oog_stack[-1] += 1 if isinstance(element, Stack_Equipment): if year >= element.year_online: stack_equipment[-1] += 1 if isinstance(element, Empty_Handler): if year >= element.year_online: empty_handler[-1] += 1 if isinstance(element, Gate): if year >= element.year_online: gates[-1] += 1 # tractor_trailer = [x / 10 for x in tractor_trailer] # 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', 'red'] offset = 4.5 * 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="STS cranes", color=colors[2], edgecolor='darkgrey') ax1.bar([x - offset + 3 * width for x in years], tractor_trailer, zorder=1, width=width, alpha=alpha, label="tractor_trailers", color=colors[3], edgecolor='darkgrey') ax1.bar([x - offset + 4 * width for x in years], laden_reefer_stack, zorder=1, width=width, alpha=alpha, label="laden / reefer stack", color=colors[4], edgecolor='darkgrey') ax1.bar([x - offset + 5 * width for x in years], empty_stack, zorder=1, width=width, alpha=alpha, label="empty stack", color=colors[5], edgecolor='darkgrey') ax1.bar([x - offset + 6 * width for x in years], oog_stack, zorder=1, width=width, alpha=alpha, label="oog stack", color=colors[6], edgecolor='darkgrey') ax1.bar([x - offset + 7 * width for x in years], stack_equipment, zorder=1, width=width, alpha=alpha, label="stack equipment", color=colors[7], edgecolor='darkgrey') ax1.bar([x - offset + 8 * width for x in years], empty_handler, zorder=1, width=width, alpha=alpha, label="empty handlers", color=colors[8], edgecolor='darkgrey') ax1.bar([x - offset + 9 * width for x in years], gates, zorder=1, width=width, alpha=alpha, label="gates", color=colors[9], edgecolor='darkgrey') # 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 # Making a second graph ax2 = ax1.twinx() ax2.step(years, demand['demand'].values, zorder=2, label="Demand [teu/y]", where='mid', color='blue') # 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(tractor_trailer), max(laden_reefer_stack), max(empty_stack), max(oog_stack), max(stack_equipment), max(empty_handler), max(gates)]) ax1.set_yticks([x for x in range(0, max_elements + 1 + 2, 10)]) ax1.set_yticklabels([int(x) for x in range(0, max_elements + 1 + 2, 10)], 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.23)
[docs] def land_use_plot(self, width=0.25, alpha=0.6, fontsize=20): """Gather data from Terminal and plot which elements come online when""" # get land use years = [] quay_land_use = [] stack_land_use = [] empty_land_use = [] oog_land_use = [] gate_land_use = [] general_land_use = [] for year in range(self.startyear, self.startyear + self.lifecycle): years.append(year) quay_land_use.append(0) stack_land_use.append(0) empty_land_use.append(0) oog_land_use.append(0) gate_land_use.append(0) general_land_use.append(0) for element in self.elements: if isinstance(element, Quay_wall): if year >= element.year_online: quay_land_use[-1] += element.land_use if isinstance(element, Laden_Stack): if year >= element.year_online: stack_land_use[-1] += element.land_use if isinstance(element, Empty_Stack): if year >= element.year_online: empty_land_use[-1] += element.land_use if isinstance(element, OOG_Stack): if year >= element.year_online: oog_land_use[-1] += element.land_use if isinstance(element, Gate): if year >= element.year_online: gate_land_use[-1] += element.land_use if isinstance(element, General_Services): if year >= element.year_online: general_land_use[-1] += element.land_use quay_land_use = [x * 0.0001 for x in quay_land_use] stack_land_use = [x * 0.0001 for x in stack_land_use] empty_land_use = [x * 0.0001 for x in empty_land_use] oog_land_use = [x * 0.0001 for x in oog_land_use] gate_land_use = [x * 0.0001 for x in gate_land_use] general_land_use = [x * 0.0001 for x in general_land_use] quay_stack = np.add(quay_land_use, stack_land_use).tolist() quay_stack_empty = np.add(quay_stack, empty_land_use).tolist() quay_stack_empty_oog = np.add(quay_stack_empty, oog_land_use).tolist() quay_stack_empty_oog_gate = np.add(quay_stack_empty_oog, gate_land_use).tolist() # generate plot fig, ax = plt.subplots(figsize=(20, 12)) ax.grid(zorder=0, which='major', axis='both') offset = 0 * width ax.bar([x - offset for x in years], quay_land_use, width=width, alpha=alpha, label="apron") ax.bar([x - offset for x in years], stack_land_use, width=width, alpha=alpha, label="laden and reefer stack", bottom=quay_land_use) ax.bar([x - offset for x in years], empty_land_use, width=width, alpha=alpha, label="empty stack", bottom=quay_stack) ax.bar([x - offset for x in years], oog_land_use, width=width, alpha=alpha, label="oog stack", bottom=quay_stack_empty) ax.bar([x - offset for x in years], gate_land_use, width=width, alpha=alpha, label="gate area", bottom=quay_stack_empty_oog) ax.bar([x - offset for x in years], general_land_use, width=width, alpha=alpha, label="general service area", bottom=quay_stack_empty_oog_gate) # title and labels ax.set_title('Terminal land use ' + self.stack_equipment, fontsize=fontsize) ax.set_xlabel('Years', fontsize=fontsize) ax.set_ylabel('Land use [ha]', 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) ticks = ax.get_yticks() ax.set_yticks([x for x in ticks]) ax.set_yticklabels([int(x) for x in ticks], fontsize=fontsize) # print legend fig.legend(loc='lower center', bbox_to_anchor=(0, -.01, .9, 0.7), fancybox=True, shadow=True, ncol=3, fontsize=fontsize) fig.subplots_adjust(bottom=0.18)
[docs] def terminal_capacity_plot(self, width=0.25, alpha=0.6): """Gather data from Terminal and plot which elements come online when""" # get crane service capacity and storage capacity years = [] cranes = [] cranes_capacity = [] for year in range(self.startyear, self.startyear + self.lifecycle): years.append(year) cranes.append(0) cranes_capacity.append(0) fully_cellular_calls, panamax_calls, panamax_max_calls, post_panamax_I_calls, post_panamax_II_calls, \ new_panamax_calls, VLCS_calls, ULCS_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, fully_cellular_calls, panamax_calls, panamax_max_calls, post_panamax_I_calls, post_panamax_II_calls, new_panamax_calls, VLCS_calls, ULCS_calls) for element in self.elements: if isinstance(element, Cyclic_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 # 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.bar([x + 0.5 * width for x in years], storages_capacity, width=width, alpha=alpha, label="storages", # color='green') ax.step(years, demand['demand'].values, label="demand", where='mid') ax.set_xlabel('Years') ax.set_ylabel('Throughput capacity [TEU/year]') ax.set_title('Terminal capacity online ({})'.format(self.crane_type_defaults['crane_type'])) ax.set_xticks([x for x in years]) ax.set_xticklabels(years) ax.legend()
[docs] def laden_stack_area_plot(self, width=0.25, alpha=0.6): """Gather data from laden stack area and plot it against demand""" # collect elements to add to plot years = [] area = [] for year in range(self.startyear, self.startyear + self.lifecycle): years.append(year) area.append(0) # stack_capacity_online, stack_capacity_planned, required_capacity, \ # total_ground_slots, laden_stack_area, reefer_capacity = \ # self.laden_reefer_stack_capacity(year) for element in self.elements: if isinstance(element, Laden_Stack): if year >= element.year_online: area[-1] += element.land_use # laden_stack_area # transform from m2 to hectares area = [x * 0.0001 for x in area] # 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, ax1 = plt.subplots(figsize=(20, 10)) ax1.set_xticks([x for x in years]) ax1.set_xticklabels(years) ax1.set_xlabel('Years') ax1.set_ylabel('Laden stack area [ha]') ax1.bar([x - 0.5 * width for x in years], area, width=width, alpha=alpha, label="laden stack area", color='red') ax2 = ax1.twinx() ax2.step(years, demand['demand'].values, label="demand", where='mid') ax2.set_ylabel('Throughput capacity [TEU/year]') ax2.set_title('Terminal capacity online ({})'.format(self.crane_type_defaults['crane_type'])) ax1.legend() ax2.legend()
[docs] def opex_plot(self, cash_flows): """Gather data from Terminal elements and combine into a cash flow plot""" # prepare years, revenue, capex and opex for plotting years = cash_flows['year'].values insurance = cash_flows['insurance'].values maintenance = cash_flows['maintenance'].values energy = cash_flows['energy'].values labour = cash_flows['labour'].values fuel = cash_flows['fuel'].values # demurrage = cash_flows['demurrage'].values print(cash_flows) # generate plot fig, ax = plt.subplots(figsize=(14, 5)) ax.step(years, insurance, label='insurance', where='mid') ax.step(years, labour, label='labour', where='mid') ax.step(years, fuel, label='fuel', where='mid') ax.step(years, energy, label='energy', where='mid') ax.step(years, maintenance, label='maintenance', where='mid') ax.set_xlabel('Years') ax.set_ylabel('Opex [ M $]') ax.set_title('Overview of Opex') ax.set_xticks([x for x in years]) ax.set_xticklabels(years) ax.legend()