Simulating two vessels passing a lock#

In this notebook, we simulate a lock on a network which two opposingly directed vessels have to pass. We add a pre-coded lock-complex object on the graph: only one vessel can be levelled at a time. The second vessel waits until the first vessel has passed the lock.

0. Import libraries#

# package(s) used for creating and geo-locating the graph
import networkx as nx
import pyproj
from shapely.geometry import Point, LineString
from shapely.ops import transform

# package(s) related to the simulation (creating the vessel, running the simulation)
import datetime
import simpy
import opentnsim
from opentnsim.graph import mixins as graph_module
from opentnsim.core.logutils import logbook2eventtable
from opentnsim.core.plotutils import generate_vessel_gantt_chart

# import of modules important for locking
from opentnsim.lock import lock as lock_module
from opentnsim.vessel_traffic_service import vessel_traffic_service as vessel_traffic_service_module

# package(s) needed for inspecting the output
import pandas as pd
import matplotlib.pyplot as plt

print("This notebook is executed with OpenTNSim version {}".format(opentnsim.__version__))
This notebook is executed with OpenTNSim version 1.3.7

1. Define object classes#

# make your preferred Vessel class out of available mix-ins.
Vessel = type(
    "Vessel", 
    (
        lock_module.PassesLockComplex,             # allows to interact with a lock
        opentnsim.core.Identifiable,               # allows to give the object a name and a random ID,
        opentnsim.core.Movable,                    # allows the object to move, with a fixed speed, while logging this activity
        opentnsim.core.VesselProperties,           # allows vessel to have dimensions, namely a length (L), width (B), and draught (T)
        opentnsim.core.ExtraMetadata,              # allow additional information, such as an arrival time (required for passing a lock)
        opentnsim.mixins.HasMultiDiGraph,           # allow to operate on a graph that can include parallel edges from and to the same nodes
        opentnsim.output.HasOutput,                # allow additional output to be stored
    ), 
    {}
)

2. Create graph#

# define reference systems
wgs84eqd = pyproj.CRS('4087')
wgs84rad = pyproj.CRS('4326')

# define transformer functions
wgs84eqd_to_wgs84rad = pyproj.transformer.Transformer.from_crs(wgs84eqd,wgs84rad,always_xy=True).transform #equidistant wgs84 to radial wgs84
wgs84rad_to_wgs84eqd = pyproj.transformer.Transformer.from_crs(wgs84rad,wgs84eqd,always_xy=True).transform #radial wgs84 to equidistant wgs84

# create a directed graph
graph = nx.DiGraph()

# add nodes
graph.add_node('0',geometry=transform(wgs84eqd_to_wgs84rad, Point(-5000,0)))
graph.add_node('1',geometry=transform(wgs84eqd_to_wgs84rad, Point(5000,0)))

# add edges
graph.add_edge('0','1', geometry = transform(wgs84eqd_to_wgs84rad, LineString([Point(-5000, 0),Point(5000, 0)])), weight=1)
graph.add_edge('1','0', weight=1); #it is not required to have a geometry
graph_module.plot_graph(graph)

3. Run simulation#

def mission(env, vessel):
    """
    Method that defines the mission of the vessel.
    
    In this case: 
        keep moving along the path until its end point is reached
    """
    while True:
        yield from vessel.move()
        
        if vessel.geometry == nx.get_node_attributes(env.graph, "geometry")[vessel.route[-1]]:
            break
# start simpy environment
simulation_start = datetime.datetime(2025, 1, 1, 0, 0, 0)
env = simpy.Environment(initial_time=simulation_start.timestamp())
env.epoch = simulation_start

# add graph to environment
env.graph = graph

# add components important for locking to the environment
env.vessel_traffic_service = vessel_traffic_service_module.VesselTrafficService(graph=graph, crs_m="4087")

lock = lock_module.IsLockComplex(
    env=env,
    name='Lock',
    node_open='0',
    node_A = '0',
    node_B = '1',
    distance_lock_doors_A_to_waiting_area_A = 4800,
    distance_lock_doors_B_to_waiting_area_B = 4800,
    distance_from_start_node_to_lock_doors_A = 4800,
    distance_from_end_node_to_lock_doors_B = 4800,
    lock_length = 400,
    lock_width = 50,
    lock_depth = 15,
    levelling_time = 300,
    sailing_distance_to_crossing_point = 1800,
    doors_opening_time= 300,
    doors_closing_time= 300,
    speed_reduction_factor_lock_chamber=0.5,
    sailing_in_time_gap_through_doors = 300,
    sailing_in_speed_sea = 1.5,
    sailing_in_speed_canal = 1.5,
    sailing_out_time_gap_through_doors = 120,
    sailing_time_before_opening_lock_doors = 600,
    sailing_time_before_closing_lock_doors = 120,
    registration_nodes = ['0','1'],
    predictive=False
)

# create vessels from dict 
data_vessel_1 = {
    "env": env,                                          # needed for simpy simulation
    "name": "Vessel 1",                                  # required by Identifiable
    "geometry": env.graph.nodes['0']['geometry'],        # required by Locatable
    "route": nx.dijkstra_path(env.graph, "0", "1"),      # required by Routeable
    "v": 4,                                              # required by Movable, 4 m/s to check if the distance is covered in the expected time
    "L": 100,                                            # required by VesselProperties, interacts with the lock capacity
    "B": 20,                                             # required by VesselProperties
    "T": 10,                                             # required by VesselProperties
    "type": 'tanker',                                    # required by VesselProperties
    "arrival_time": pd.Timestamp('2025-01-01 00:00:00')  # required by PassesLockComplex
}  
vessel_1 = Vessel(**data_vessel_1)
vessel_1.name = 'Vessel 1'

data_vessel_2 = {
    "env": env,                                          # needed for simpy simulation
    "name": "Vessel 2",                                  # required by Identifiable
    "geometry": env.graph.nodes['1']['geometry'],        # required by Locatable
    "route": nx.dijkstra_path(env.graph, "1", "0"),      # required by Routeable
    "v": 4,                                              # required by Movable, 4 m/s to check if the distance is covered in the expected time
    "L": 100,                                            # required by VesselProperties, interacts with the lock capacity
    "B": 20,                                             # required by VesselProperties
    "T": 10,                                             # required by VesselProperties
    "type": 'tanker',                                    # required by VesselProperties
    "arrival_time": pd.Timestamp('2025-01-01 00:05:00')  # required by PassesLockComplex
}  
vessel_2 = Vessel(**data_vessel_2)
vessel_2.name = 'Vessel 2'

# start the simulation
env.process(mission(env, vessel_1))
env.process(mission(env, vessel_2))
env.run()
lock.vessel_planning
id node_from node_to lock_chamber L B T operation_index time_of_registration time_of_acceptance ... time_arrival_at_lineup_area time_lock_passing_start time_lock_entry_start time_lock_entry_stop time_lock_departure_start time_lock_departure_stop time_lock_passing_stop time_potential_lock_door_closure_start direction delay
0 4a233d34-e0b0-49a4-8921-a884b57bda28 0 1 NaN 100 20 10 0 2025-01-01 00:00:00 2025-01-01 00:00:00 ... NaN 2025-01-01 00:12:30 2025-01-01 00:20:00 2025-01-01 00:25:40.172786470 2025-01-01 00:40:40.172786470 2025-01-01 00:41:28.768898822 2025-01-01 00:48:58.768898822 2025-01-01 00:25:40.172786470 0.0 0 days 00:00:00
1 fb00dddd-9822-47c3-8b45-7d639d6d2ad9 1 0 NaN 100 20 10 1 2025-01-01 00:05:00 2025-01-01 00:05:00 ... NaN 2025-01-01 00:48:58.768898822 2025-01-01 00:56:28.768898822 2025-01-01 01:02:08.941685292 2025-01-01 01:17:08.941685292 2025-01-01 01:17:57.537797644 2025-01-01 01:25:27.537797644 2025-01-01 01:02:08.941685292 1.0 0 days 00:31:28.768898822

2 rows × 22 columns

lock.operation_planning
node_from node_to direction lock_chamber vessels capacity_L capacity_B time_potential_lock_door_opening_stop time_operation_start time_entry_start ... time_door_opening_stop time_departure_start time_departure_stop time_operation_stop time_potential_lock_door_closure_start wlev_A wlev_B maximum_individual_delay total_delay status
lock_operation
0 0 1 0 Lock [<__main__.Vessel object at 0x7fa4b9ff0c20>] 300 30 2025-01-01 00:10:00 2025-01-01 00:12:30 2025-01-01 00:20:00 ... 2025-01-01 00:40:40.172786470 2025-01-01 00:40:40.172786470 2025-01-01 00:41:28.768898822 2025-01-01 00:48:58.768898822 2025-01-01 00:43:28.768898822 NaN NaN 0 days 00:00:00 0 days 00:00:00 unavailable
1 1 0 1 Lock [<__main__.Vessel object at 0x7fa4ba1bd950>] 300 30 2025-01-01 00:46:28.768898822 2025-01-01 00:48:58.768898822 2025-01-01 00:56:28.768898822 ... 2025-01-01 01:17:08.941685292 2025-01-01 01:17:08.941685292 2025-01-01 01:17:57.537797644 2025-01-01 01:25:27.537797644 2025-01-01 01:19:57.537797644 NaN NaN 0 days 00:31:28.768898822 0 days 00:31:28.768898822 unavailable

2 rows × 26 columns

4. Inspect output#

# load the logbook data into a dataframe
df = pd.DataFrame.from_dict(vessel_1.logbook)

print("'{}' logbook data:".format(vessel_1.name))  
print('')

display(df)
'Vessel 1' logbook data:
Message Timestamp Value Geometry
0 Sailing from node 0 to node 1 start 2025-01-01 00:00:00.000000 0 POINT (-0.0449157642059761 0)
1 Sailing to first lock doors start 2025-01-01 00:00:00.000000 {'origin': '', 'destination': '', 'route': [],... POINT (-0.0449157642059761 0)
2 Sailing to first lock doors stop 2025-01-01 00:20:00.000000 {'origin': '', 'destination': '', 'route': [],... POINT (-0.001796630568239 0)
3 Sailing to position in lock start 2025-01-01 00:20:00.000000 {'origin': '', 'destination': '', 'route': [],... POINT (-0.001796630568239 0)
4 Sailing to position in lock stop 2025-01-01 00:25:40.172786 {'origin': '', 'destination': '', 'route': [],... POINT (0.0013474729261793 0)
5 Levelling start 2025-01-01 00:30:40.172786 {'origin': '', 'destination': '', 'route': [],... POINT (0.0013474729261793 0)
6 Levelling stop 2025-01-01 00:35:40.172786 {'origin': '', 'destination': '', 'route': [],... POINT (0.0013474729261793 0)
7 Sailing to second lock doors start 2025-01-01 00:40:40.172786 {'origin': '', 'destination': '', 'route': [],... POINT (0.0013474729261793 0)
8 Sailing to second lock doors stop 2025-01-01 00:41:28.768899 {'origin': '', 'destination': '', 'route': [],... POINT (0.001796630568239 0)
9 Sailing to lock complex exit start 2025-01-01 00:41:28.768899 {'origin': '', 'destination': '', 'route': [],... POINT (0.001796630568239 0)
10 Sailing to lock complex exit stop 2025-01-01 01:01:28.768899 {'origin': '', 'destination': '', 'route': [],... POINT (0.0449157642059761 0)
11 Sailing from node 0 to node 1 stop 2025-01-01 01:01:28.768899 0 POINT (0.0449157642059761 0)
# load the logbook data into a dataframe
df = pd.DataFrame.from_dict(vessel_2.logbook)

print("'{}' logbook data:".format(vessel_2.name))  
print('')

display(df)
'Vessel 2' logbook data:
Message Timestamp Value Geometry
0 Sailing from node 1 to node 0 start 2025-01-01 00:05:00.000000 0 POINT (0.0449157642059761 0)
1 Waiting for lock operation start 2025-01-01 00:05:00.000000 {'origin': '', 'destination': '', 'route': [],... POINT (0.0449157642059761 0)
2 Waiting for lock operation stop 2025-01-01 00:36:28.768898 {'origin': '', 'destination': '', 'route': [],... POINT (0.0449157642059761 0)
3 Sailing to first lock doors start 2025-01-01 00:36:28.768898 {'origin': '', 'destination': '', 'route': [],... POINT (0.0449157642059761 0)
4 Sailing to first lock doors stop 2025-01-01 00:56:28.768898 {'origin': '', 'destination': '', 'route': [],... POINT (0.001796630568239 0)
5 Sailing to position in lock start 2025-01-01 00:56:28.768898 {'origin': '', 'destination': '', 'route': [],... POINT (0.001796630568239 0)
6 Sailing to position in lock stop 2025-01-01 01:02:08.941684 {'origin': '', 'destination': '', 'route': [],... POINT (-0.0013474729261793 0)
7 Levelling start 2025-01-01 01:07:09.000000 {'origin': '', 'destination': '', 'route': [],... POINT (-0.0013474729261793 0)
8 Levelling stop 2025-01-01 01:12:09.000000 {'origin': '', 'destination': '', 'route': [],... POINT (-0.0013474729261793 0)
9 Sailing to second lock doors start 2025-01-01 01:17:09.000000 {'origin': '', 'destination': '', 'route': [],... POINT (-0.0013474729261793 0)
10 Sailing to second lock doors stop 2025-01-01 01:17:57.596112 {'origin': '', 'destination': '', 'route': [],... POINT (-0.001796630568239 0)
11 Sailing to lock complex exit start 2025-01-01 01:17:57.596112 {'origin': '', 'destination': '', 'route': [],... POINT (-0.001796630568239 0)
12 Sailing to lock complex exit stop 2025-01-01 01:37:57.596112 {'origin': '', 'destination': '', 'route': [],... POINT (-0.0449157642059761 0)
13 Sailing from node 1 to node 0 stop 2025-01-01 01:37:57.596112 0 POINT (-0.0449157642059761 0)
# load the logbook data into a dataframe
lock_df = pd.DataFrame.from_dict(lock.lock_chamber.logbook)

print("'{}' logbook data:".format(lock.name))  
print('')

display(lock_df)
'Lock' logbook data:
Message Timestamp Value Geometry
0 Lock doors closing start 2025-01-01 00:25:40.172786 {} 0
1 Lock doors closing stop 2025-01-01 00:30:40.172786 {} 0
2 Lock chamber converting start 2025-01-01 00:30:40.172786 {} 0
3 Lock chamber converting stop 2025-01-01 00:35:40.172786 {} 1
4 Lock doors opening start 2025-01-01 00:35:40.172786 {} 1
5 Lock doors opening stop 2025-01-01 00:40:40.172786 {} 1
6 Lock doors closing start 2025-01-01 01:02:09.000000 {} 1
7 Lock doors closing stop 2025-01-01 01:07:09.000000 {} 1
8 Lock chamber converting start 2025-01-01 01:07:09.000000 {} 1
9 Lock chamber converting stop 2025-01-01 01:12:09.000000 {} 0
10 Lock doors opening start 2025-01-01 01:12:09.000000 {} 0
11 Lock doors opening stop 2025-01-01 01:17:09.000000 {} 0
%load_ext autoreload
%autoreload 2
# We can plot the time-distance diagram
fig = lock.create_time_distance_plot(vessels = [vessel_1,vessel_2], 
                                     xlimmin = -5050, 
                                     xlimmax = 5050, 
                                     method='Plotly')
fig.show()
df_eventtable = opentnsim.core.logutils.logbook2eventtable([vessel_1, vessel_2, lock.lock_chamber])
generate_vessel_gantt_chart(df_eventtable)