simulation_classes.terminal_liquid

Uses liquid bulk terminal class objects to create liquid terminal processes. Vessel arrivals, berth and pipeline allocation, cargo unloading, cargo loading, vessel waiting, and vessel departure are simulated.

  1"""
  2Uses liquid bulk terminal class objects to create liquid terminal processes.
  3Vessel arrivals, berth and pipeline allocation, cargo unloading, cargo loading, vessel waiting, and vessel departure are simulated. 
  4"""
  5from simulation_analysis.results import update_ship_logs
  6from simulation_handler.helpers import get_value_by_terminal, is_daytime
  7from simulation_classes.pipeline import Pipeline
  8import constants
  9import simpy
 10
 11LIQUID_TERMINAL_EFFICIENCY = constants.LIQ_TERMINAL_EFFICIENCY
 12TIME_COMMON_CHANNEL = constants.TIME_COMMON_CHANNEL
 13TIME_FOR_TUG_STEER = constants.TIME_FOR_TUG_STEER
 14TIME_FOR_UTURN = constants.TIME_FOR_UTURN
 15
 16nan = float('nan')
 17
 18
 19class LiquidTerminal:
 20    """
 21    LiquidTerminal class simulates the liquid bulk terminal operations, including ship arrivals, berthing, unloading, loading, waiting, and departure.
 22    It manages the allocation of berths and pipelines, handles cargo operations, and logs events related to the terminal operations.
 23    This class is initialized with various parameters such as the simulation environment, channel, pilots, tugboats, ship information, terminal data, and more.
 24    The processes include the following steps:
 25    1. Ship arrival and anchorage waiting.
 26    2. Berth allocation.
 27    3. Channel process for entering the terminal.
 28    4. Unloading of liquid bulk cargo.
 29    5. Loading of liquid bulk cargo.
 30    6. Waiting time at the terminal.
 31    7. Detachment from the berth and departure from the terminal.
 32    8. Channel process for exiting the terminal.
 33    The class also handles the allocation of pipelines based on the terminal type (import/export) and manages the storage of cargo in port tanks.
 34    It logs all events and updates ship logs throughout the process.
 35    
 36    Args:
 37        env (simpy.Environment): The simulation environment.
 38        chassis_bays_utilization (float): Utilization of chassis bays.
 39        run_id (str): Unique identifier for the simulation run.
 40        channel (Channel): The channel object for managing ship movements.
 41        day_pilots (simpy.Container): Simpy container for day pilots available.
 42        night_pilots (simpy.Container) : Simpy container for night pilots available.    
 43        tugboats (simpy.Container): Simpy container for tugboats available.
 44        ship_info (dict): Information about the ships.
 45        last_section (str): The last section of the channel.
 46        selected_terminal (str): The terminal selected for the ship.
 47        id (int): Unique identifier for the ship.
 48        ship_type (str): Type of the ship (e.g., "Liquid").
 49        draft (float): Draft of the ship.
 50        width (float): Width of the ship.
 51        unload_time (float): Time taken to unload the ship.
 52        load_time (float): Time taken to load the ship.
 53        unload_tons (float): Amount of cargo to unload from the ship.
 54        load_tons (float): Amount of cargo to load onto the ship.
 55        events (list): List to store events related to the ship.
 56        ship_logs (dict): Dictionary to store logs for the ship.
 57        port_berths (simpy.Resource): Resource for managing port berths.
 58        port_tanks (simpy.Container): Container for managing port tanks.
 59        SHIPS_IN_CHANNEL (int): Number of ships currently in the channel.
 60        SHIPS_IN_ANCHORAGE (int): Number of ships currently in the anchorage.
 61        terminal_data (dict): Data related to the terminal.
 62        liq_terminals_with_pipeline_source (list): List of liquid terminals with pipeline source.
 63        liq_terminals_with_pipeline_sink (list): List of liquid terminals with pipeline sink.
 64    """
 65    def __init__(self, env, chassis_bays_utilization, run_id, channel, day_pilots, night_pilots, tugboats, ship_info, last_section, selected_terminal, id, ship_type, draft, width, unload_time, load_time, unload_tons, load_tons, events, ship_logs, port_berths, port_tanks, SHIPS_IN_CHANNEL, SHIPS_IN_ANCHORAGE, terminal_data, liq_terminals_with_pipeline_source, liq_terminals_with_pipeline_sink):
 66        # Initialize the environment
 67        self.env = env
 68        self.run_id = run_id
 69        self.SHIPS_IN_CHANNEL = SHIPS_IN_CHANNEL
 70        self.SHIPS_IN_ANCHORAGE = SHIPS_IN_ANCHORAGE
 71        self.terminal_data = terminal_data
 72
 73        # Initialize the channel
 74        self.channel = channel
 75        self.ship_info = ship_info
 76        self.last_section = last_section
 77
 78        # Pilots and tugboats
 79        self.day_pilots, self.night_pilots = day_pilots, night_pilots
 80        self.tugboats = tugboats
 81
 82        # Set vessel and terminal attributes
 83        self.id = id
 84        self.selected_terminal = selected_terminal
 85        self.ship_type = ship_type
 86        self.num_pipelines = get_value_by_terminal(
 87            self.terminal_data, 'Liquid', selected_terminal, 'transfer units per berth')
 88
 89        # Set vessel dimensions and cargo handling parameters
 90        self.unload_tons = unload_tons
 91        self.load_tons = load_tons
 92        self.draft = draft
 93        self.width = width
 94        self.unload_time = unload_time
 95        self.load_time = load_time
 96        self.day = None
 97
 98        # Cargo wait time
 99        self.cargo_wait_time = 0
100
101        # Pipelines
102        self.liq_terminals_with_pipeline_source = liq_terminals_with_pipeline_source
103        self.liq_terminals_with_pipeline_sink = liq_terminals_with_pipeline_sink
104        self.time_for_uturn_min, self.time_for_uturn_max = TIME_FOR_UTURN
105
106        # The allocation of pipelines and berth will be done during the process
107        self.current_berth = None
108        self.allocated_pipelines = []
109
110        # Get the ship logs and events
111        self.events = events
112        self.ship_logs = ship_logs
113
114        # Allocate berth and pipelines
115        self.port_berths = port_berths
116        self.port_tanks = port_tanks
117
118        # Start the process
119        self.env.process(self.process())
120
121    def process(self):
122        """
123        Process for the liquid bulk terminal, simulating the arrival, berthing, unloading, loading, waiting, and departure of ships.
124        This method handles the entire lifecycle of a ship at the terminal, including logging events and updating ship logs."""
125
126        self.ship_logs = update_ship_logs(self.ship_logs, "L", self.id, self.selected_terminal, ship_start_time=nan, time_to_get_berth=nan, time_for_restriction_in=nan, time_to_get_pilot_in=nan,
127                                          time_to_get_tugs_in=nan, time_to_common_channel_in=nan, time_to_travel_channel_in=nan, time_to_tug_steer_in=nan, unloading_time=nan, loading_time=nan, waiting_time=nan,
128                                          departure_time=nan, time_to_get_pilot_out=nan, time_to_get_tugs_out=nan, time_to_tug_steer_out=nan, time_for_uturn=nan,
129                                          time_to_travel_channel_out=nan, time_to_common_channel_out=nan, ship_end_time=nan)
130
131        # ship arrival
132        ship_start_time = self.env.now
133        update_ship_logs(self.ship_logs, "L", self.id,
134                         self.selected_terminal, ship_start_time=ship_start_time)
135        self.events.append((f"Ship_{self.id}", "Liquid", f"L.{self.selected_terminal}",
136                           "arrive", self.env.now, f"Ship {self.id} arrived at the port"))
137
138        # Anchorage waiting
139        yield self.env.timeout(constants.ANCHORAGE_WAITING_LIQUID)
140
141        # Berth allocation
142        start_time = self.env.now
143        yield self.env.process(self.arrive_and_berth())
144        time_to_get_berth = self.env.now - ship_start_time
145        update_ship_logs(self.ship_logs, "L", self.id,
146                         self.selected_terminal, time_to_get_berth=time_to_get_berth)
147
148        # Channel process (in)
149        self.day = is_daytime(self.env.now, 7, 19)
150        yield self.env.process(self.channel.channel_process(self.ship_info, self.day, self.last_section, self.id, self.selected_terminal, "L", self.run_id))
151
152        # Terminal process
153        self.events.append((f"Ship_{self.id}", "Liquid", f"L.{self.selected_terminal}", "dock", self.env.now,
154                           f"Ship {self.id} docked to berth {self.current_berth.id} with waiting time {'N/A'}"))
155        start_time = self.env.now
156        yield self.env.process(self.unload())
157        unloading_time = self.env.now - start_time
158        update_ship_logs(self.ship_logs, "L", self.id,
159                         self.selected_terminal, unloading_time=unloading_time)
160
161        start_time = self.env.now
162        yield self.env.process(self.load())
163        loading_time = self.env.now - start_time
164        update_ship_logs(self.ship_logs, "L", self.id,
165                         self.selected_terminal, loading_time=loading_time)
166
167        start_time = self.env.now
168        yield self.env.process(self.wait())
169        waiting_time = self.env.now - start_time
170        update_ship_logs(self.ship_logs, "L", self.id,
171                         self.selected_terminal, waiting_time=waiting_time)
172
173        start_time = self.env.now
174        yield self.env.process(self.detach_and_depart())
175        departure_time = self.env.now - start_time
176        update_ship_logs(self.ship_logs, "L", self.id,
177                         self.selected_terminal, departure_time=departure_time)
178
179        self.events.append((f"Ship_{self.id}", "Liquid", f"L.{self.selected_terminal}", "undock", self.env.now,
180                           f"Ship {self.id} undocked to berth {self.current_berth.id} with waiting time {'N/A'}"))
181
182        # Channel process (out)
183        self.day = is_daytime(self.env.now, 7, 19)
184        yield self.env.process(self.channel.channel_process(self.ship_info, self.day, self.last_section, self.id, self.selected_terminal, "L", self.run_id))
185        ship_end_time = self.env.now
186        update_ship_logs(self.ship_logs, "L", self.id,
187                         self.selected_terminal, ship_end_time=ship_end_time)
188        self.events.append((f"Ship_{self.id}", "Liquid", f"L.{self.selected_terminal}", "depart", self.env.now,
189                           f"Ship {self.id} departed from berth {self.current_berth.id} with waiting time {'N/A'}"))
190
191    def arrive_and_berth(self):
192        """
193        Represents the arrival of the ship at the terminal and allocation of a berth.
194        This method waits for a berth to become available and allocates it to the ship.
195        """
196        # Wait for a berth to become available
197        self.current_berth = yield self.port_berths.get()
198
199    def unload(self):
200        """
201        Represents the unloading of liquid bulk cargo from the ship at the terminal.
202        This method checks if the terminal is exporting or importing, allocates pipelines, and unloads the cargo.
203        It also handles the storage of the unloaded cargo in the port tanks and ensures that the tanks have enough capacity.
204        If the port tanks are full, it activates a pipeline sink to empty the tanks.
205        If the port tanks are empty, it activates a pipeline source to fill the tanks.
206        It also ensures that the unloading process does not exceed the tank capacity.
207        The method logs events related to the unloading process.
208        """
209
210        # check if terminal is exporting or importing
211        import_terminal = get_value_by_terminal(
212            self.terminal_data, 'Liquid', self.selected_terminal, 'import')
213        if import_terminal:
214            # Request for the required number of pipelines
215            for _ in range(self.num_pipelines):
216                pipeline = yield self.current_berth.pipelines.get()
217                self.allocated_pipelines.append(pipeline)
218            self.events.append((f"Ship_{self.id}", "Liquid", f"L.{self.selected_terminal}",
219                               "all_pipelines_attached", self.env.now, f"Ship {self.id} pipelines allocated"))
220
221            # Unload liquid bulk cargo
222            total_unload_time = self.unload_tons * self.unload_time / self.num_pipelines
223            self.cargo_wait_time += total_unload_time
224            yield self.env.timeout(total_unload_time)
225            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
226                               "all_liquid_unloaded", self.env.now, "Tanker {} unloaded all liquid".format(self.id)))
227
228            # Put liquid batches in tanks
229            if self.port_tanks.level + self.unload_tons >= 0.9 * self.port_tanks.capacity:
230                if self.selected_terminal in self.liq_terminals_with_pipeline_sink:
231                    Pipeline(self.run_id, self.env, self.port_tanks,
232                             mode='sink', rate=constants.PIPELINE_RATE)
233                    with open(f'.{self.run_id}/logs/force_action.txt', 'a') as f:
234                        f.write(f"Pipeline sink activated {self.env.now}")
235                        f.write('\n')
236
237            # Do not offload until there is enough space in the port tanks (New feature added on 2025-03-24) #TODO: Check if this is the right way to do it
238            while self.port_tanks.level + self.unload_tons >= self.port_tanks.capacity:
239                yield self.env.timeout(1)
240
241            yield self.port_tanks.put(self.unload_tons)
242
243            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
244                               "all_liquid_stored", self.env.now, "Tanker {} unloaded all liquid".format(self.id)))
245        else:
246            self.events.append((f"Ship_{self.id}", "Liquid", f"L.{self.selected_terminal}",
247                               "all_pipelines_attached", self.env.now, f"Ship {self.id} pipelines allocated"))
248            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
249                               "all_liquid_unloaded", self.env.now, "Tanker {} unloaded all liquid".format(self.id)))
250            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
251                               "all_liquid_stored", self.env.now, "Tanker {} unloaded all liquid".format(self.id)))
252
253    def load(self):
254        """
255        Represents the loading of liquid bulk cargo onto the ship at the terminal.
256        This method checks if the terminal is exporting or importing, allocates pipelines, and loads the cargo.
257        It also handles the storage of the loaded cargo in the port tanks and ensures that the tanks have enough capacity.
258        If the port tanks are empty, it activates a pipeline source to fill the tanks.
259        It also ensures that the loading process does not exceed the tank capacity.
260        The method logs events related to the loading process.
261        """
262
263        export_terminal = get_value_by_terminal(
264            self.terminal_data, 'Liquid', self.selected_terminal, 'export')
265        if export_terminal:
266            # Represents liquid bulk cargo being loaded onto the vessel at the input pump rate
267            total_load_time = self.load_tons * self.load_time / self.num_pipelines
268            # Load liquid bulk cargo
269            if self.port_tanks.level - self.load_tons <= 0.1 * self.port_tanks.capacity:
270                if self.selected_terminal in self.liq_terminals_with_pipeline_source:
271                    Pipeline(self.run_id, self.env, self.port_tanks,
272                             mode='source', rate=constants.PIPELINE_RATE)
273                    with open(f'.{self.run_id}/logs/force_action.txt', 'a') as f:
274                        f.write(f"Pipeline source activated {self.env.now}")
275                        f.write('\n')
276
277            # Do not load until there is enough liquid in the port tanks (New feature added on 2025-03-24) #TODO: Check if this is the right way to do it
278            while self.port_tanks.level - self.load_tons <= 0.0 * self.port_tanks.capacity:
279                yield self.env.timeout(1)
280
281            yield self.port_tanks.get(self.load_tons)
282            self.cargo_wait_time += total_load_time
283            yield self.env.timeout(total_load_time)
284            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
285                               "all_liquid_loaded", self.env.now, "Tanker {} loaded all liquid".format(self.id)))
286        else:
287            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
288                               "all_liquid_loaded", self.env.now, "Tanker {} loaded all liquid".format(self.id)))
289
290    def wait(self):
291        """
292        Represents the waiting time of the vessel at the terminal after loading or unloading.
293        This method simulates the waiting time before the vessel departs the terminal.
294        It calculates the waiting time based on the liquid terminal efficiency and the cargo wait time.
295        The method logs events related to the waiting process.
296        """
297        port_waiting_time = (
298            1/LIQUID_TERMINAL_EFFICIENCY - 1) * self.cargo_wait_time
299        yield self.env.timeout(port_waiting_time)
300        self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
301                           "wait", self.env.now, "Tanker {} waited at the port".format(self.id)))
302
303    def detach_and_depart(self):
304        """
305        Represents the detachment of the ship from the berth and its departure from the terminal.
306        This method releases the allocated pipelines, puts the berth back into the port berths resource, and logs events related to the detachment and departure process.
307        """
308        # Release the pipelines
309        for pipeline in self.allocated_pipelines:
310            yield self.current_berth.pipelines.put(pipeline)
311            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}", "all_pipelines_disconnected",
312                               self.env.now, "Tanker {} disconnected from pipeline {}".format(self.id, pipeline.id)))
313
314        # Depart from the berth
315        yield self.port_berths.put(self.current_berth)
class LiquidTerminal:
 20class LiquidTerminal:
 21    """
 22    LiquidTerminal class simulates the liquid bulk terminal operations, including ship arrivals, berthing, unloading, loading, waiting, and departure.
 23    It manages the allocation of berths and pipelines, handles cargo operations, and logs events related to the terminal operations.
 24    This class is initialized with various parameters such as the simulation environment, channel, pilots, tugboats, ship information, terminal data, and more.
 25    The processes include the following steps:
 26    1. Ship arrival and anchorage waiting.
 27    2. Berth allocation.
 28    3. Channel process for entering the terminal.
 29    4. Unloading of liquid bulk cargo.
 30    5. Loading of liquid bulk cargo.
 31    6. Waiting time at the terminal.
 32    7. Detachment from the berth and departure from the terminal.
 33    8. Channel process for exiting the terminal.
 34    The class also handles the allocation of pipelines based on the terminal type (import/export) and manages the storage of cargo in port tanks.
 35    It logs all events and updates ship logs throughout the process.
 36    
 37    Args:
 38        env (simpy.Environment): The simulation environment.
 39        chassis_bays_utilization (float): Utilization of chassis bays.
 40        run_id (str): Unique identifier for the simulation run.
 41        channel (Channel): The channel object for managing ship movements.
 42        day_pilots (simpy.Container): Simpy container for day pilots available.
 43        night_pilots (simpy.Container) : Simpy container for night pilots available.    
 44        tugboats (simpy.Container): Simpy container for tugboats available.
 45        ship_info (dict): Information about the ships.
 46        last_section (str): The last section of the channel.
 47        selected_terminal (str): The terminal selected for the ship.
 48        id (int): Unique identifier for the ship.
 49        ship_type (str): Type of the ship (e.g., "Liquid").
 50        draft (float): Draft of the ship.
 51        width (float): Width of the ship.
 52        unload_time (float): Time taken to unload the ship.
 53        load_time (float): Time taken to load the ship.
 54        unload_tons (float): Amount of cargo to unload from the ship.
 55        load_tons (float): Amount of cargo to load onto the ship.
 56        events (list): List to store events related to the ship.
 57        ship_logs (dict): Dictionary to store logs for the ship.
 58        port_berths (simpy.Resource): Resource for managing port berths.
 59        port_tanks (simpy.Container): Container for managing port tanks.
 60        SHIPS_IN_CHANNEL (int): Number of ships currently in the channel.
 61        SHIPS_IN_ANCHORAGE (int): Number of ships currently in the anchorage.
 62        terminal_data (dict): Data related to the terminal.
 63        liq_terminals_with_pipeline_source (list): List of liquid terminals with pipeline source.
 64        liq_terminals_with_pipeline_sink (list): List of liquid terminals with pipeline sink.
 65    """
 66    def __init__(self, env, chassis_bays_utilization, run_id, channel, day_pilots, night_pilots, tugboats, ship_info, last_section, selected_terminal, id, ship_type, draft, width, unload_time, load_time, unload_tons, load_tons, events, ship_logs, port_berths, port_tanks, SHIPS_IN_CHANNEL, SHIPS_IN_ANCHORAGE, terminal_data, liq_terminals_with_pipeline_source, liq_terminals_with_pipeline_sink):
 67        # Initialize the environment
 68        self.env = env
 69        self.run_id = run_id
 70        self.SHIPS_IN_CHANNEL = SHIPS_IN_CHANNEL
 71        self.SHIPS_IN_ANCHORAGE = SHIPS_IN_ANCHORAGE
 72        self.terminal_data = terminal_data
 73
 74        # Initialize the channel
 75        self.channel = channel
 76        self.ship_info = ship_info
 77        self.last_section = last_section
 78
 79        # Pilots and tugboats
 80        self.day_pilots, self.night_pilots = day_pilots, night_pilots
 81        self.tugboats = tugboats
 82
 83        # Set vessel and terminal attributes
 84        self.id = id
 85        self.selected_terminal = selected_terminal
 86        self.ship_type = ship_type
 87        self.num_pipelines = get_value_by_terminal(
 88            self.terminal_data, 'Liquid', selected_terminal, 'transfer units per berth')
 89
 90        # Set vessel dimensions and cargo handling parameters
 91        self.unload_tons = unload_tons
 92        self.load_tons = load_tons
 93        self.draft = draft
 94        self.width = width
 95        self.unload_time = unload_time
 96        self.load_time = load_time
 97        self.day = None
 98
 99        # Cargo wait time
100        self.cargo_wait_time = 0
101
102        # Pipelines
103        self.liq_terminals_with_pipeline_source = liq_terminals_with_pipeline_source
104        self.liq_terminals_with_pipeline_sink = liq_terminals_with_pipeline_sink
105        self.time_for_uturn_min, self.time_for_uturn_max = TIME_FOR_UTURN
106
107        # The allocation of pipelines and berth will be done during the process
108        self.current_berth = None
109        self.allocated_pipelines = []
110
111        # Get the ship logs and events
112        self.events = events
113        self.ship_logs = ship_logs
114
115        # Allocate berth and pipelines
116        self.port_berths = port_berths
117        self.port_tanks = port_tanks
118
119        # Start the process
120        self.env.process(self.process())
121
122    def process(self):
123        """
124        Process for the liquid bulk terminal, simulating the arrival, berthing, unloading, loading, waiting, and departure of ships.
125        This method handles the entire lifecycle of a ship at the terminal, including logging events and updating ship logs."""
126
127        self.ship_logs = update_ship_logs(self.ship_logs, "L", self.id, self.selected_terminal, ship_start_time=nan, time_to_get_berth=nan, time_for_restriction_in=nan, time_to_get_pilot_in=nan,
128                                          time_to_get_tugs_in=nan, time_to_common_channel_in=nan, time_to_travel_channel_in=nan, time_to_tug_steer_in=nan, unloading_time=nan, loading_time=nan, waiting_time=nan,
129                                          departure_time=nan, time_to_get_pilot_out=nan, time_to_get_tugs_out=nan, time_to_tug_steer_out=nan, time_for_uturn=nan,
130                                          time_to_travel_channel_out=nan, time_to_common_channel_out=nan, ship_end_time=nan)
131
132        # ship arrival
133        ship_start_time = self.env.now
134        update_ship_logs(self.ship_logs, "L", self.id,
135                         self.selected_terminal, ship_start_time=ship_start_time)
136        self.events.append((f"Ship_{self.id}", "Liquid", f"L.{self.selected_terminal}",
137                           "arrive", self.env.now, f"Ship {self.id} arrived at the port"))
138
139        # Anchorage waiting
140        yield self.env.timeout(constants.ANCHORAGE_WAITING_LIQUID)
141
142        # Berth allocation
143        start_time = self.env.now
144        yield self.env.process(self.arrive_and_berth())
145        time_to_get_berth = self.env.now - ship_start_time
146        update_ship_logs(self.ship_logs, "L", self.id,
147                         self.selected_terminal, time_to_get_berth=time_to_get_berth)
148
149        # Channel process (in)
150        self.day = is_daytime(self.env.now, 7, 19)
151        yield self.env.process(self.channel.channel_process(self.ship_info, self.day, self.last_section, self.id, self.selected_terminal, "L", self.run_id))
152
153        # Terminal process
154        self.events.append((f"Ship_{self.id}", "Liquid", f"L.{self.selected_terminal}", "dock", self.env.now,
155                           f"Ship {self.id} docked to berth {self.current_berth.id} with waiting time {'N/A'}"))
156        start_time = self.env.now
157        yield self.env.process(self.unload())
158        unloading_time = self.env.now - start_time
159        update_ship_logs(self.ship_logs, "L", self.id,
160                         self.selected_terminal, unloading_time=unloading_time)
161
162        start_time = self.env.now
163        yield self.env.process(self.load())
164        loading_time = self.env.now - start_time
165        update_ship_logs(self.ship_logs, "L", self.id,
166                         self.selected_terminal, loading_time=loading_time)
167
168        start_time = self.env.now
169        yield self.env.process(self.wait())
170        waiting_time = self.env.now - start_time
171        update_ship_logs(self.ship_logs, "L", self.id,
172                         self.selected_terminal, waiting_time=waiting_time)
173
174        start_time = self.env.now
175        yield self.env.process(self.detach_and_depart())
176        departure_time = self.env.now - start_time
177        update_ship_logs(self.ship_logs, "L", self.id,
178                         self.selected_terminal, departure_time=departure_time)
179
180        self.events.append((f"Ship_{self.id}", "Liquid", f"L.{self.selected_terminal}", "undock", self.env.now,
181                           f"Ship {self.id} undocked to berth {self.current_berth.id} with waiting time {'N/A'}"))
182
183        # Channel process (out)
184        self.day = is_daytime(self.env.now, 7, 19)
185        yield self.env.process(self.channel.channel_process(self.ship_info, self.day, self.last_section, self.id, self.selected_terminal, "L", self.run_id))
186        ship_end_time = self.env.now
187        update_ship_logs(self.ship_logs, "L", self.id,
188                         self.selected_terminal, ship_end_time=ship_end_time)
189        self.events.append((f"Ship_{self.id}", "Liquid", f"L.{self.selected_terminal}", "depart", self.env.now,
190                           f"Ship {self.id} departed from berth {self.current_berth.id} with waiting time {'N/A'}"))
191
192    def arrive_and_berth(self):
193        """
194        Represents the arrival of the ship at the terminal and allocation of a berth.
195        This method waits for a berth to become available and allocates it to the ship.
196        """
197        # Wait for a berth to become available
198        self.current_berth = yield self.port_berths.get()
199
200    def unload(self):
201        """
202        Represents the unloading of liquid bulk cargo from the ship at the terminal.
203        This method checks if the terminal is exporting or importing, allocates pipelines, and unloads the cargo.
204        It also handles the storage of the unloaded cargo in the port tanks and ensures that the tanks have enough capacity.
205        If the port tanks are full, it activates a pipeline sink to empty the tanks.
206        If the port tanks are empty, it activates a pipeline source to fill the tanks.
207        It also ensures that the unloading process does not exceed the tank capacity.
208        The method logs events related to the unloading process.
209        """
210
211        # check if terminal is exporting or importing
212        import_terminal = get_value_by_terminal(
213            self.terminal_data, 'Liquid', self.selected_terminal, 'import')
214        if import_terminal:
215            # Request for the required number of pipelines
216            for _ in range(self.num_pipelines):
217                pipeline = yield self.current_berth.pipelines.get()
218                self.allocated_pipelines.append(pipeline)
219            self.events.append((f"Ship_{self.id}", "Liquid", f"L.{self.selected_terminal}",
220                               "all_pipelines_attached", self.env.now, f"Ship {self.id} pipelines allocated"))
221
222            # Unload liquid bulk cargo
223            total_unload_time = self.unload_tons * self.unload_time / self.num_pipelines
224            self.cargo_wait_time += total_unload_time
225            yield self.env.timeout(total_unload_time)
226            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
227                               "all_liquid_unloaded", self.env.now, "Tanker {} unloaded all liquid".format(self.id)))
228
229            # Put liquid batches in tanks
230            if self.port_tanks.level + self.unload_tons >= 0.9 * self.port_tanks.capacity:
231                if self.selected_terminal in self.liq_terminals_with_pipeline_sink:
232                    Pipeline(self.run_id, self.env, self.port_tanks,
233                             mode='sink', rate=constants.PIPELINE_RATE)
234                    with open(f'.{self.run_id}/logs/force_action.txt', 'a') as f:
235                        f.write(f"Pipeline sink activated {self.env.now}")
236                        f.write('\n')
237
238            # Do not offload until there is enough space in the port tanks (New feature added on 2025-03-24) #TODO: Check if this is the right way to do it
239            while self.port_tanks.level + self.unload_tons >= self.port_tanks.capacity:
240                yield self.env.timeout(1)
241
242            yield self.port_tanks.put(self.unload_tons)
243
244            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
245                               "all_liquid_stored", self.env.now, "Tanker {} unloaded all liquid".format(self.id)))
246        else:
247            self.events.append((f"Ship_{self.id}", "Liquid", f"L.{self.selected_terminal}",
248                               "all_pipelines_attached", self.env.now, f"Ship {self.id} pipelines allocated"))
249            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
250                               "all_liquid_unloaded", self.env.now, "Tanker {} unloaded all liquid".format(self.id)))
251            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
252                               "all_liquid_stored", self.env.now, "Tanker {} unloaded all liquid".format(self.id)))
253
254    def load(self):
255        """
256        Represents the loading of liquid bulk cargo onto the ship at the terminal.
257        This method checks if the terminal is exporting or importing, allocates pipelines, and loads the cargo.
258        It also handles the storage of the loaded cargo in the port tanks and ensures that the tanks have enough capacity.
259        If the port tanks are empty, it activates a pipeline source to fill the tanks.
260        It also ensures that the loading process does not exceed the tank capacity.
261        The method logs events related to the loading process.
262        """
263
264        export_terminal = get_value_by_terminal(
265            self.terminal_data, 'Liquid', self.selected_terminal, 'export')
266        if export_terminal:
267            # Represents liquid bulk cargo being loaded onto the vessel at the input pump rate
268            total_load_time = self.load_tons * self.load_time / self.num_pipelines
269            # Load liquid bulk cargo
270            if self.port_tanks.level - self.load_tons <= 0.1 * self.port_tanks.capacity:
271                if self.selected_terminal in self.liq_terminals_with_pipeline_source:
272                    Pipeline(self.run_id, self.env, self.port_tanks,
273                             mode='source', rate=constants.PIPELINE_RATE)
274                    with open(f'.{self.run_id}/logs/force_action.txt', 'a') as f:
275                        f.write(f"Pipeline source activated {self.env.now}")
276                        f.write('\n')
277
278            # Do not load until there is enough liquid in the port tanks (New feature added on 2025-03-24) #TODO: Check if this is the right way to do it
279            while self.port_tanks.level - self.load_tons <= 0.0 * self.port_tanks.capacity:
280                yield self.env.timeout(1)
281
282            yield self.port_tanks.get(self.load_tons)
283            self.cargo_wait_time += total_load_time
284            yield self.env.timeout(total_load_time)
285            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
286                               "all_liquid_loaded", self.env.now, "Tanker {} loaded all liquid".format(self.id)))
287        else:
288            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
289                               "all_liquid_loaded", self.env.now, "Tanker {} loaded all liquid".format(self.id)))
290
291    def wait(self):
292        """
293        Represents the waiting time of the vessel at the terminal after loading or unloading.
294        This method simulates the waiting time before the vessel departs the terminal.
295        It calculates the waiting time based on the liquid terminal efficiency and the cargo wait time.
296        The method logs events related to the waiting process.
297        """
298        port_waiting_time = (
299            1/LIQUID_TERMINAL_EFFICIENCY - 1) * self.cargo_wait_time
300        yield self.env.timeout(port_waiting_time)
301        self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
302                           "wait", self.env.now, "Tanker {} waited at the port".format(self.id)))
303
304    def detach_and_depart(self):
305        """
306        Represents the detachment of the ship from the berth and its departure from the terminal.
307        This method releases the allocated pipelines, puts the berth back into the port berths resource, and logs events related to the detachment and departure process.
308        """
309        # Release the pipelines
310        for pipeline in self.allocated_pipelines:
311            yield self.current_berth.pipelines.put(pipeline)
312            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}", "all_pipelines_disconnected",
313                               self.env.now, "Tanker {} disconnected from pipeline {}".format(self.id, pipeline.id)))
314
315        # Depart from the berth
316        yield self.port_berths.put(self.current_berth)

LiquidTerminal class simulates the liquid bulk terminal operations, including ship arrivals, berthing, unloading, loading, waiting, and departure. It manages the allocation of berths and pipelines, handles cargo operations, and logs events related to the terminal operations. This class is initialized with various parameters such as the simulation environment, channel, pilots, tugboats, ship information, terminal data, and more. The processes include the following steps:

  1. Ship arrival and anchorage waiting.
  2. Berth allocation.
  3. Channel process for entering the terminal.
  4. Unloading of liquid bulk cargo.
  5. Loading of liquid bulk cargo.
  6. Waiting time at the terminal.
  7. Detachment from the berth and departure from the terminal.
  8. Channel process for exiting the terminal. The class also handles the allocation of pipelines based on the terminal type (import/export) and manages the storage of cargo in port tanks. It logs all events and updates ship logs throughout the process.
Arguments:
  • env (simpy.Environment): The simulation environment.
  • chassis_bays_utilization (float): Utilization of chassis bays.
  • run_id (str): Unique identifier for the simulation run.
  • channel (Channel): The channel object for managing ship movements.
  • day_pilots (simpy.Container): Simpy container for day pilots available.
  • night_pilots (simpy.Container) : Simpy container for night pilots available.
  • tugboats (simpy.Container): Simpy container for tugboats available.
  • ship_info (dict): Information about the ships.
  • last_section (str): The last section of the channel.
  • selected_terminal (str): The terminal selected for the ship.
  • id (int): Unique identifier for the ship.
  • ship_type (str): Type of the ship (e.g., "Liquid").
  • draft (float): Draft of the ship.
  • width (float): Width of the ship.
  • unload_time (float): Time taken to unload the ship.
  • load_time (float): Time taken to load the ship.
  • unload_tons (float): Amount of cargo to unload from the ship.
  • load_tons (float): Amount of cargo to load onto the ship.
  • events (list): List to store events related to the ship.
  • ship_logs (dict): Dictionary to store logs for the ship.
  • port_berths (simpy.Resource): Resource for managing port berths.
  • port_tanks (simpy.Container): Container for managing port tanks.
  • SHIPS_IN_CHANNEL (int): Number of ships currently in the channel.
  • SHIPS_IN_ANCHORAGE (int): Number of ships currently in the anchorage.
  • terminal_data (dict): Data related to the terminal.
  • liq_terminals_with_pipeline_source (list): List of liquid terminals with pipeline source.
  • liq_terminals_with_pipeline_sink (list): List of liquid terminals with pipeline sink.
def process(self):
122    def process(self):
123        """
124        Process for the liquid bulk terminal, simulating the arrival, berthing, unloading, loading, waiting, and departure of ships.
125        This method handles the entire lifecycle of a ship at the terminal, including logging events and updating ship logs."""
126
127        self.ship_logs = update_ship_logs(self.ship_logs, "L", self.id, self.selected_terminal, ship_start_time=nan, time_to_get_berth=nan, time_for_restriction_in=nan, time_to_get_pilot_in=nan,
128                                          time_to_get_tugs_in=nan, time_to_common_channel_in=nan, time_to_travel_channel_in=nan, time_to_tug_steer_in=nan, unloading_time=nan, loading_time=nan, waiting_time=nan,
129                                          departure_time=nan, time_to_get_pilot_out=nan, time_to_get_tugs_out=nan, time_to_tug_steer_out=nan, time_for_uturn=nan,
130                                          time_to_travel_channel_out=nan, time_to_common_channel_out=nan, ship_end_time=nan)
131
132        # ship arrival
133        ship_start_time = self.env.now
134        update_ship_logs(self.ship_logs, "L", self.id,
135                         self.selected_terminal, ship_start_time=ship_start_time)
136        self.events.append((f"Ship_{self.id}", "Liquid", f"L.{self.selected_terminal}",
137                           "arrive", self.env.now, f"Ship {self.id} arrived at the port"))
138
139        # Anchorage waiting
140        yield self.env.timeout(constants.ANCHORAGE_WAITING_LIQUID)
141
142        # Berth allocation
143        start_time = self.env.now
144        yield self.env.process(self.arrive_and_berth())
145        time_to_get_berth = self.env.now - ship_start_time
146        update_ship_logs(self.ship_logs, "L", self.id,
147                         self.selected_terminal, time_to_get_berth=time_to_get_berth)
148
149        # Channel process (in)
150        self.day = is_daytime(self.env.now, 7, 19)
151        yield self.env.process(self.channel.channel_process(self.ship_info, self.day, self.last_section, self.id, self.selected_terminal, "L", self.run_id))
152
153        # Terminal process
154        self.events.append((f"Ship_{self.id}", "Liquid", f"L.{self.selected_terminal}", "dock", self.env.now,
155                           f"Ship {self.id} docked to berth {self.current_berth.id} with waiting time {'N/A'}"))
156        start_time = self.env.now
157        yield self.env.process(self.unload())
158        unloading_time = self.env.now - start_time
159        update_ship_logs(self.ship_logs, "L", self.id,
160                         self.selected_terminal, unloading_time=unloading_time)
161
162        start_time = self.env.now
163        yield self.env.process(self.load())
164        loading_time = self.env.now - start_time
165        update_ship_logs(self.ship_logs, "L", self.id,
166                         self.selected_terminal, loading_time=loading_time)
167
168        start_time = self.env.now
169        yield self.env.process(self.wait())
170        waiting_time = self.env.now - start_time
171        update_ship_logs(self.ship_logs, "L", self.id,
172                         self.selected_terminal, waiting_time=waiting_time)
173
174        start_time = self.env.now
175        yield self.env.process(self.detach_and_depart())
176        departure_time = self.env.now - start_time
177        update_ship_logs(self.ship_logs, "L", self.id,
178                         self.selected_terminal, departure_time=departure_time)
179
180        self.events.append((f"Ship_{self.id}", "Liquid", f"L.{self.selected_terminal}", "undock", self.env.now,
181                           f"Ship {self.id} undocked to berth {self.current_berth.id} with waiting time {'N/A'}"))
182
183        # Channel process (out)
184        self.day = is_daytime(self.env.now, 7, 19)
185        yield self.env.process(self.channel.channel_process(self.ship_info, self.day, self.last_section, self.id, self.selected_terminal, "L", self.run_id))
186        ship_end_time = self.env.now
187        update_ship_logs(self.ship_logs, "L", self.id,
188                         self.selected_terminal, ship_end_time=ship_end_time)
189        self.events.append((f"Ship_{self.id}", "Liquid", f"L.{self.selected_terminal}", "depart", self.env.now,
190                           f"Ship {self.id} departed from berth {self.current_berth.id} with waiting time {'N/A'}"))

Process for the liquid bulk terminal, simulating the arrival, berthing, unloading, loading, waiting, and departure of ships. This method handles the entire lifecycle of a ship at the terminal, including logging events and updating ship logs.

def arrive_and_berth(self):
192    def arrive_and_berth(self):
193        """
194        Represents the arrival of the ship at the terminal and allocation of a berth.
195        This method waits for a berth to become available and allocates it to the ship.
196        """
197        # Wait for a berth to become available
198        self.current_berth = yield self.port_berths.get()

Represents the arrival of the ship at the terminal and allocation of a berth. This method waits for a berth to become available and allocates it to the ship.

def unload(self):
200    def unload(self):
201        """
202        Represents the unloading of liquid bulk cargo from the ship at the terminal.
203        This method checks if the terminal is exporting or importing, allocates pipelines, and unloads the cargo.
204        It also handles the storage of the unloaded cargo in the port tanks and ensures that the tanks have enough capacity.
205        If the port tanks are full, it activates a pipeline sink to empty the tanks.
206        If the port tanks are empty, it activates a pipeline source to fill the tanks.
207        It also ensures that the unloading process does not exceed the tank capacity.
208        The method logs events related to the unloading process.
209        """
210
211        # check if terminal is exporting or importing
212        import_terminal = get_value_by_terminal(
213            self.terminal_data, 'Liquid', self.selected_terminal, 'import')
214        if import_terminal:
215            # Request for the required number of pipelines
216            for _ in range(self.num_pipelines):
217                pipeline = yield self.current_berth.pipelines.get()
218                self.allocated_pipelines.append(pipeline)
219            self.events.append((f"Ship_{self.id}", "Liquid", f"L.{self.selected_terminal}",
220                               "all_pipelines_attached", self.env.now, f"Ship {self.id} pipelines allocated"))
221
222            # Unload liquid bulk cargo
223            total_unload_time = self.unload_tons * self.unload_time / self.num_pipelines
224            self.cargo_wait_time += total_unload_time
225            yield self.env.timeout(total_unload_time)
226            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
227                               "all_liquid_unloaded", self.env.now, "Tanker {} unloaded all liquid".format(self.id)))
228
229            # Put liquid batches in tanks
230            if self.port_tanks.level + self.unload_tons >= 0.9 * self.port_tanks.capacity:
231                if self.selected_terminal in self.liq_terminals_with_pipeline_sink:
232                    Pipeline(self.run_id, self.env, self.port_tanks,
233                             mode='sink', rate=constants.PIPELINE_RATE)
234                    with open(f'.{self.run_id}/logs/force_action.txt', 'a') as f:
235                        f.write(f"Pipeline sink activated {self.env.now}")
236                        f.write('\n')
237
238            # Do not offload until there is enough space in the port tanks (New feature added on 2025-03-24) #TODO: Check if this is the right way to do it
239            while self.port_tanks.level + self.unload_tons >= self.port_tanks.capacity:
240                yield self.env.timeout(1)
241
242            yield self.port_tanks.put(self.unload_tons)
243
244            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
245                               "all_liquid_stored", self.env.now, "Tanker {} unloaded all liquid".format(self.id)))
246        else:
247            self.events.append((f"Ship_{self.id}", "Liquid", f"L.{self.selected_terminal}",
248                               "all_pipelines_attached", self.env.now, f"Ship {self.id} pipelines allocated"))
249            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
250                               "all_liquid_unloaded", self.env.now, "Tanker {} unloaded all liquid".format(self.id)))
251            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
252                               "all_liquid_stored", self.env.now, "Tanker {} unloaded all liquid".format(self.id)))

Represents the unloading of liquid bulk cargo from the ship at the terminal. This method checks if the terminal is exporting or importing, allocates pipelines, and unloads the cargo. It also handles the storage of the unloaded cargo in the port tanks and ensures that the tanks have enough capacity. If the port tanks are full, it activates a pipeline sink to empty the tanks. If the port tanks are empty, it activates a pipeline source to fill the tanks. It also ensures that the unloading process does not exceed the tank capacity. The method logs events related to the unloading process.

def load(self):
254    def load(self):
255        """
256        Represents the loading of liquid bulk cargo onto the ship at the terminal.
257        This method checks if the terminal is exporting or importing, allocates pipelines, and loads the cargo.
258        It also handles the storage of the loaded cargo in the port tanks and ensures that the tanks have enough capacity.
259        If the port tanks are empty, it activates a pipeline source to fill the tanks.
260        It also ensures that the loading process does not exceed the tank capacity.
261        The method logs events related to the loading process.
262        """
263
264        export_terminal = get_value_by_terminal(
265            self.terminal_data, 'Liquid', self.selected_terminal, 'export')
266        if export_terminal:
267            # Represents liquid bulk cargo being loaded onto the vessel at the input pump rate
268            total_load_time = self.load_tons * self.load_time / self.num_pipelines
269            # Load liquid bulk cargo
270            if self.port_tanks.level - self.load_tons <= 0.1 * self.port_tanks.capacity:
271                if self.selected_terminal in self.liq_terminals_with_pipeline_source:
272                    Pipeline(self.run_id, self.env, self.port_tanks,
273                             mode='source', rate=constants.PIPELINE_RATE)
274                    with open(f'.{self.run_id}/logs/force_action.txt', 'a') as f:
275                        f.write(f"Pipeline source activated {self.env.now}")
276                        f.write('\n')
277
278            # Do not load until there is enough liquid in the port tanks (New feature added on 2025-03-24) #TODO: Check if this is the right way to do it
279            while self.port_tanks.level - self.load_tons <= 0.0 * self.port_tanks.capacity:
280                yield self.env.timeout(1)
281
282            yield self.port_tanks.get(self.load_tons)
283            self.cargo_wait_time += total_load_time
284            yield self.env.timeout(total_load_time)
285            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
286                               "all_liquid_loaded", self.env.now, "Tanker {} loaded all liquid".format(self.id)))
287        else:
288            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
289                               "all_liquid_loaded", self.env.now, "Tanker {} loaded all liquid".format(self.id)))

Represents the loading of liquid bulk cargo onto the ship at the terminal. This method checks if the terminal is exporting or importing, allocates pipelines, and loads the cargo. It also handles the storage of the loaded cargo in the port tanks and ensures that the tanks have enough capacity. If the port tanks are empty, it activates a pipeline source to fill the tanks. It also ensures that the loading process does not exceed the tank capacity. The method logs events related to the loading process.

def wait(self):
291    def wait(self):
292        """
293        Represents the waiting time of the vessel at the terminal after loading or unloading.
294        This method simulates the waiting time before the vessel departs the terminal.
295        It calculates the waiting time based on the liquid terminal efficiency and the cargo wait time.
296        The method logs events related to the waiting process.
297        """
298        port_waiting_time = (
299            1/LIQUID_TERMINAL_EFFICIENCY - 1) * self.cargo_wait_time
300        yield self.env.timeout(port_waiting_time)
301        self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}",
302                           "wait", self.env.now, "Tanker {} waited at the port".format(self.id)))

Represents the waiting time of the vessel at the terminal after loading or unloading. This method simulates the waiting time before the vessel departs the terminal. It calculates the waiting time based on the liquid terminal efficiency and the cargo wait time. The method logs events related to the waiting process.

def detach_and_depart(self):
304    def detach_and_depart(self):
305        """
306        Represents the detachment of the ship from the berth and its departure from the terminal.
307        This method releases the allocated pipelines, puts the berth back into the port berths resource, and logs events related to the detachment and departure process.
308        """
309        # Release the pipelines
310        for pipeline in self.allocated_pipelines:
311            yield self.current_berth.pipelines.put(pipeline)
312            self.events.append(("Tanker_{}".format(self.id), "Liquid", f"L.{self.selected_terminal}", "all_pipelines_disconnected",
313                               self.env.now, "Tanker {} disconnected from pipeline {}".format(self.id, pipeline.id)))
314
315        # Depart from the berth
316        yield self.port_berths.put(self.current_berth)

Represents the detachment of the ship from the berth and its departure from the terminal. This method releases the allocated pipelines, puts the berth back into the port berths resource, and logs events related to the detachment and departure process.