Energy calculation along a route#

In this notebook we set up a basic simulation where one vessel moves over a 1D network path consisting of three edges each 100 km long. The middle section has a shallower depth than the other sections. The calculations show the estimated energy consumption along the route.

0. Import libraries#

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

# package(s) related to the simulation (creating the vessel, running the simulation)
import datetime, time
import simpy
import opentnsim
from opentnsim.core.logutils import logbook2eventtable
from opentnsim.energy.logutils import (
    add_energy_attributes_to_eventtable,
    add_fuel_attributes_to_event_table,
)

# package(s) needed for inspecting the output
import pandas as pd

# package(s) needed for plotting
import plotly.graph_objects as go
from plotly.subplots import make_subplots

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", 
    (
        opentnsim.energy.mixins.ConsumesEnergy,
        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.vessel_properties.VesselProperties,
        opentnsim.core.ExtraMetadata,
    ), 
    {}
)

2. Create graph#

Next we create a network (a graph) along which the vessel can move. This case we create a single edge of 100 km exactly.

# initialize geodetic calculator with WGS84 ellipsoid
geod = Geod(ellps="WGS84")
# starting point (longitude, latitude)
lon0, lat0 = 0, 0

# compute the other point 100 km East from Point 0
lon1, lat1, _ = geod.fwd(lon0, lat0, 90, 100000) # East from Point 0
lon2, lat2, _ = geod.fwd(lon1, lat1, 90, 100000) # East from Point 1
lon3, lat3, _ = geod.fwd(lon2, lat2, 90, 100000) # East from Point 2

# define nodes with their geographic coordinates
coords = {
    "0": (lon0, lat0),
    "1": (lon1, lat1),
    "2": (lon2, lat2),
    "3": (lon3, lat3),
}
# create list of edges
edges = [
    ("0", "1"), ("1", "0"),
    ("1", "2"), ("2", "1"),
    ("2", "3"), ("3", "2"),
] # bi-directional edge
# create a directed graph
FG = nx.DiGraph()

# add nodes
for name, coord in coords.items():
    FG.add_node(name, geometry=Point(coord[0], coord[1]))

# add edges
for edge in edges:
    FG.add_edge(edge[0], edge[1], weight=1)
opentnsim.graph.plot_graph(FG)

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(2024, 1, 1, 0, 0, 0)
env = simpy.Environment(initial_time=simulation_start.timestamp())
env.epoch = simulation_start

# add graph to environment
FG.edges['0', '1']['GeneralDepth'] = 6
FG.edges['1', '0']['GeneralDepth'] = 6
FG.edges['1', '2']['GeneralDepth'] = 4.5
FG.edges['2', '1']['GeneralDepth'] = 4.5
FG.edges['2', '3']['GeneralDepth'] = 6
FG.edges['3', '2']['GeneralDepth'] = 6
env.graph = FG

# create vessel from a dict 
route = nx.dijkstra_path(env.graph, "0", "3")
data_vessel = {
    "env": env,
    "name": 'Vessel',           # you can give the vessel an arbitratry name
    "type": 'Va/M9 - Verl. Groot Rijnschip', # This indicates the vessel class. This info is mainly informative.
    "L": 110,                   # m
    "B": 11.4,                  # m
    "T": 3.5,                   # m
    "v": 3,                     # m/s If None: this value is calculated based on P_tot_given
    "safety_margin": 0.2,       # for tanker vessel with sandy bed the safety margin is recommended as 0.2 m 
    "h_squat": False,            # if the ship should squat while moving, set to True, otherwise set to False
    "P_installed": 1750.0,      # kW
    "P_tot_given": None,        # kW If None: this value is calculated value based on speed
    "bulbous_bow": False,       # if a vessel has no bulbous_bow, set to False; otherwise set to True.
    "karpov_correction": False, # if False, don't apply the karpov correction, if True, apply the karpov correction
    "P_hotel_perc": 0.05,       # 0: all power goes to propulsion
    "P_hotel": None,            # None: calculate P_hotel from percentage
    "x": 2,                     # number of propellers
    "L_w": 3.0 ,
    "C_B": 0.85,                # block coefficient 
    "C_year": 1990,             # engine build year
    "arrival_time": datetime.datetime(2024, 1, 1, 0, 0, 0),
    "geometry": env.graph.nodes[route[0]]['geometry'],
    "route": route,             # the route to sail
}  # 

# create an instance of the Vessel class using the input dict data_vessel
vessel = Vessel(**data_vessel)

# start the simulation
env.process(mission(env, vessel))
env.run()

4. Inspect output#

We can now inspect the simulation output by inspecting the vessel.logbook. Note that the Log mix-in was included when we added Movable. The vessel.logbook keeps track of the moving activities of the vessel. For each discrete event OpenTNSim logs an event message, the start/stop time and the location. The vessel.logbook is of type dict. For convenient inspection it can be loaded into a Pandas dataframe.

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

display(df)
Message Timestamp Value Geometry
0 Sailing from node 0 to node 1 start 2024-01-01 00:00:00.000000 0.0 POINT (0 0)
1 Sailing from node 0 to node 1 stop 2024-01-01 09:15:33.333333 100000.0 POINT (0.8983152841195217 0)
2 Sailing from node 1 to node 2 start 2024-01-01 09:15:33.333333 100000.0 POINT (0.8983152841195217 0)
3 Sailing from node 1 to node 2 stop 2024-01-01 18:31:06.666667 200000.0 POINT (1.7966305682390433 0)
4 Sailing from node 2 to node 3 start 2024-01-01 18:31:06.666667 200000.0 POINT (1.7966305682390433 0)
5 Sailing from node 2 to node 3 stop 2024-01-02 03:46:40.000000 300000.0 POINT (2.6949458523585648 0)

The inspection of the logbook data shows that Vessel moved from its origin (Node 0) to its destination (Node 1). The print statements show that the length of the route from Node 0 to Node 1 is exactly 100 km. At a given speed of 1 m/s the trip duration should be exactly 100000 seconds, as is indeed shown to be the case.

df_eventtable = logbook2eventtable([vessel])
df_eventtable
object id object name activity name start location stop location start time stop time distance (m) duration (s)
0 669b40b2-e60f-4ce8-a34e-5a3d5531117b Vessel Sailing from node 0 to node 1 POINT (0 0) POINT (0.8983152841195217 0) 2024-01-01 00:00:00.000000 2024-01-01 09:15:33.333333 100000.0 33333.333333
1 669b40b2-e60f-4ce8-a34e-5a3d5531117b Vessel Sailing from node 1 to node 2 POINT (0.8983152841195217 0) POINT (1.7966305682390433 0) 2024-01-01 09:15:33.333333 2024-01-01 18:31:06.666667 100000.0 33333.333334
2 669b40b2-e60f-4ce8-a34e-5a3d5531117b Vessel Sailing from node 2 to node 3 POINT (1.7966305682390433 0) POINT (2.6949458523585648 0) 2024-01-01 18:31:06.666667 2024-01-02 03:46:40.000000 100000.0 33333.333333
# add energy attributes to the minimum event table
df_eventtable_energy = add_energy_attributes_to_eventtable(df_eventtable, [vessel])        

pd.set_option('display.max_columns', None)
df_eventtable_energy
object id object name activity name start location stop location start time stop time distance (m) duration (s) waterdepth (m) waterway width (m) current (m/s) engine age (year) P_tot (kW) P_given (kW) P_installed (kW) total_energy (kWh)
0 669b40b2-e60f-4ce8-a34e-5a3d5531117b Vessel Sailing from node 0 to node 1 POINT (0 0) POINT (0.8983152841195217 0) 2024-01-01 00:00:00.000000 2024-01-01 09:15:33.333333 100000.0 33333.333333 6.0 None 0.0 1990.0 295.331755 295.331755 1750.0 2734.553287
1 669b40b2-e60f-4ce8-a34e-5a3d5531117b Vessel Sailing from node 1 to node 2 POINT (0.8983152841195217 0) POINT (1.7966305682390433 0) 2024-01-01 09:15:33.333333 2024-01-01 18:31:06.666667 100000.0 33333.333334 4.5 None 0.0 1990.0 309.180758 309.180758 1750.0 2862.784798
2 669b40b2-e60f-4ce8-a34e-5a3d5531117b Vessel Sailing from node 2 to node 3 POINT (1.7966305682390433 0) POINT (2.6949458523585648 0) 2024-01-01 18:31:06.666667 2024-01-02 03:46:40.000000 100000.0 33333.333333 6.0 None 0.0 1990.0 295.331755 295.331755 1750.0 2734.553287
# add fuel attributes to the minimum event table with energy attributes
df_eventtable_energy_fuel = add_fuel_attributes_to_event_table(df_eventtable_energy, [vessel])

pd.set_option('display.max_columns', None)
df_eventtable_energy_fuel
object id object name activity name start location stop location start time stop time distance (m) duration (s) waterdepth (m) waterway width (m) current (m/s) engine age (year) P_tot (kW) P_given (kW) P_installed (kW) total_energy (kWh) diesel_consumption (g) diesel_consumption_m (g/m) diesel_consumption_s (g/s) CO2_emission_total (g) PM10_emission_total (g) NOX_emission_total (g) CO2_emission_per_m (g/m) PM10_emission_per_m (g/m) NOX_emission_per_m (g/m) CO2_emission_per_s (g/s) PM10_emission_per_s (g/s) NOX_emission_per_s (g/s)
0 669b40b2-e60f-4ce8-a34e-5a3d5531117b Vessel Sailing from node 0 to node 1 POINT (0 0) POINT (0.8983152841195217 0) 2024-01-01 00:00:00.000000 2024-01-01 09:15:33.333333 100000.0 33333.333333 6.0 None 0.0 1990.0 295.331755 295.331755 1750.0 2734.553287 703118.042240 7.031180 21.093541 2.230802e+06 1390.489055 31588.792306 22.308018 0.013905 0.315888 66.924054 0.041715 0.947664
1 669b40b2-e60f-4ce8-a34e-5a3d5531117b Vessel Sailing from node 1 to node 2 POINT (0.8983152841195217 0) POINT (1.7966305682390433 0) 2024-01-01 09:15:33.333333 2024-01-01 18:31:06.666667 100000.0 33333.333334 4.5 None 0.0 1990.0 309.180758 309.180758 1750.0 2862.784798 736089.383323 7.360894 22.082681 2.335411e+06 1455.693311 33070.086745 23.354109 0.014557 0.330701 70.062326 0.043671 0.992103
2 669b40b2-e60f-4ce8-a34e-5a3d5531117b Vessel Sailing from node 2 to node 3 POINT (1.7966305682390433 0) POINT (2.6949458523585648 0) 2024-01-01 18:31:06.666667 2024-01-02 03:46:40.000000 100000.0 33333.333333 6.0 None 0.0 1990.0 295.331755 295.331755 1750.0 2734.553287 703118.042240 7.031180 21.093541 2.230802e+06 1390.489055 31588.792306 22.308018 0.013905 0.315888 66.924054 0.041715 0.947664
# Calculate average speed
average_speed = df_eventtable_energy_fuel['distance (m)'] / df_eventtable_energy_fuel['duration (s)']

# Calculate durations in minutes
durations = [
    (row['stop time'] - row['start time']).total_seconds() / 60
    for _, row in df_eventtable_energy_fuel.iterrows()
]

# Create 2x2 subplot figure
fig = make_subplots(rows=2, cols=2, subplot_titles=[
    'Average Speed per Section [m/s]',
    'Average Power Given per Section [kW]',
    'Total Section Transit Time [mins]',
    'Total Section Transit Energy [kWh]'
])

# Add bar charts to subplots
fig.add_trace(go.Bar(x=df.index, y=average_speed, name='Speed [m/s]'), row=1, col=1)
fig.add_trace(go.Bar(x=df.index, y=df_eventtable_energy_fuel['P_given (kW)'], name='Power Given [kW]'), row=1, col=2)
fig.add_trace(go.Bar(x=df.index, y=durations, name='Transit Time [mins]'), row=2, col=1)
fig.add_trace(go.Bar(x=df.index, y=df_eventtable_energy_fuel['total_energy (kWh)'], name='Energy [kWh]'), row=2, col=2)

# Update layout
fig.update_layout(
    height=800,
    width=1000,
    title_text='Section Analysis',
    plot_bgcolor='white'
)

fig.show()