Basic trip moving cargo#
In this notebook, we set up a basic simulation where one vessel moves over a 1D network path consisting of one edge. We add the HasContainer mixin to both the nodes of the network and the vessel. We created a simple customized mission, where the vessel sails back and forth loading/unloading until the destination site is full.
0. Import libraries#
# package(s) used for creating and geo-locating the graph
import networkx as nx
from pyproj import Geod
from shapely.geometry import Point
# package(s) related to the simulation (creating the vessel, running the simulation)
import datetime
import simpy
import opentnsim
from opentnsim.core.logutils import logbook2eventtable
from opentnsim.core.plotutils import generate_vessel_gantt_chart
# package(s) needed for inspecting the output
import pandas as pd
import numpy as np
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.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.HasContainer, # allows the object to contain cargo
),
{}
)
# make your preferred Node class out of available mix-ins.
Node = type(
"Node",
(
opentnsim.core.Identifiable, # allows to give the object a name and a random ID,
opentnsim.core.Locatable, # allows the object to have a location
opentnsim.core.HasContainer, # allows the object to contain cargo
),
{}
)
2. Create graph#
Next we create a network (a graph) along which the vessel can move. For 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 1
# define nodes with their geographic coordinates, and capacity and level
node_info = {
"0": {'position': (lon0, lat0), 'capacity': 100, 'level': 100},
"1": {'position': (lon1, lat1), 'capacity': 100, 'level': 0},
}
# create list of edges
edges = [("0", "1"), ("1", "0")] # bi-directional edge
# create a directed graph
FG = nx.DiGraph()
# add nodes
for name, node in node_info.items():
FG.add_node(name, geometry=Point(node['position'][0], node['position'][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, origin, destination):
"""
Method that defines the mission of the vessel.
In this case:
load - move - unload - move until the to_site is full
"""
while True:
# *** load ***
current_node = '0'
amount = np.min([vessel.container.capacity, env.graph.nodes['0']['site'].container.level])
duration = amount * 1000
vessel.log_entry_v0(
"Loading from node {} start".format(current_node),
vessel.env.now, 0, env.graph.nodes[current_node]['geometry'])
yield env.graph.nodes['0']['site'].container.get(amount)
yield vessel.container.put(amount)
yield env.timeout(duration)
vessel.log_entry_v0(
"Loading from node {} stop".format(current_node),
vessel.env.now, 0, env.graph.nodes[current_node]['geometry'])
# *** move ***
route = nx.dijkstra_path(env.graph, origin, destination)
vessel.route = route
while True:
yield from vessel.move()
if vessel.geometry == nx.get_node_attributes(FG, "geometry")[vessel.route[-1]]:
break
# *** unload ***
current_node = '1'
amount = vessel.container.level
duration = amount * 1000
vessel.log_entry_v0(
"Unloading to node {} start".format(current_node),
vessel.env.now, 0, env.graph.nodes[current_node]['geometry'])
yield vessel.container.get(amount)
yield env.graph.nodes['1']['site'].container.put(amount)
yield env.timeout(duration)
vessel.log_entry_v0(
"Unloading to node {} stop".format(current_node),
vessel.env.now, 0, env.graph.nodes[current_node]['geometry'])
# *** move ***
vessel.route = nx.dijkstra_path(env.graph, destination, origin)
while True:
yield from vessel.move()
if vessel.geometry == nx.get_node_attributes(FG, "geometry")[vessel.route[-1]]:
break
if env.graph.nodes['1']['site'].container.level == env.graph.nodes['1']['site'].container.capacity:
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
# create sites from dict
nodes = []
for name, node in node_info.items():
data_node = {
"env": env,
"name": name,
"geometry": Point(node['position'][0], node['position'][1]),
"capacity": node['capacity'],
"level": node['level'],
}
nodes.append(Node(**data_node))
# add sites to graph
for node in nodes:
FG.add_node(node.name, geometry=node.geometry, site=node)
# add graph to environment
env.graph = FG
# create vessel from dict
data_vessel = {
"env": env, # needed for simpy simulation, left empty for now
"name": "Vessel", # required by Identifiable
"geometry": env.graph.nodes['0']['geometry'], # required by Locatable, left empty for now
"route": [], # required by Routeable, left empty for now
"v": 1, # required by Movable, 1 m/s to check if the distance is covered in the expected time
"capacity": 30, # required by HasContainer
"level": 0, # required by HasContainer
}
vessel = Vessel(**data_vessel)
# start the simulation
env.process(mission(env, vessel, '0', '1'))
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)
print("'{}' logbook data:".format(vessel.name))
print('')
display(df)
'Vessel' logbook data:
Message | Timestamp | Value | Geometry | |
---|---|---|---|---|
0 | Loading from node 0 start | 2024-01-01 00:00:00 | 0.0 | POINT (0 0) |
1 | Loading from node 0 stop | 2024-01-01 08:20:00 | 0.0 | POINT (0 0) |
2 | Sailing from node 0 to node 1 start | 2024-01-01 08:20:00 | 0.0 | POINT (0 0) |
3 | Sailing from node 0 to node 1 stop | 2024-01-02 12:06:40 | 100000.0 | POINT (0.8983152841195217 0) |
4 | Unloading to node 1 start | 2024-01-02 12:06:40 | 0.0 | POINT (0.8983152841195217 0) |
5 | Unloading to node 1 stop | 2024-01-02 20:26:40 | 0.0 | POINT (0.8983152841195217 0) |
6 | Sailing from node 1 to node 0 start | 2024-01-02 20:26:40 | 100000.0 | POINT (0.8983152841195217 0) |
7 | Sailing from node 1 to node 0 stop | 2024-01-04 00:13:20 | 200000.0 | POINT (0 0) |
8 | Loading from node 0 start | 2024-01-04 00:13:20 | 0.0 | POINT (0 0) |
9 | Loading from node 0 stop | 2024-01-04 08:33:20 | 0.0 | POINT (0 0) |
10 | Sailing from node 0 to node 1 start | 2024-01-04 08:33:20 | 200000.0 | POINT (0 0) |
11 | Sailing from node 0 to node 1 stop | 2024-01-05 12:20:00 | 300000.0 | POINT (0.8983152841195217 0) |
12 | Unloading to node 1 start | 2024-01-05 12:20:00 | 0.0 | POINT (0.8983152841195217 0) |
13 | Unloading to node 1 stop | 2024-01-05 20:40:00 | 0.0 | POINT (0.8983152841195217 0) |
14 | Sailing from node 1 to node 0 start | 2024-01-05 20:40:00 | 300000.0 | POINT (0.8983152841195217 0) |
15 | Sailing from node 1 to node 0 stop | 2024-01-07 00:26:40 | 400000.0 | POINT (0 0) |
16 | Loading from node 0 start | 2024-01-07 00:26:40 | 0.0 | POINT (0 0) |
17 | Loading from node 0 stop | 2024-01-07 08:46:40 | 0.0 | POINT (0 0) |
18 | Sailing from node 0 to node 1 start | 2024-01-07 08:46:40 | 400000.0 | POINT (0 0) |
19 | Sailing from node 0 to node 1 stop | 2024-01-08 12:33:20 | 500000.0 | POINT (0.8983152841195217 0) |
20 | Unloading to node 1 start | 2024-01-08 12:33:20 | 0.0 | POINT (0.8983152841195217 0) |
21 | Unloading to node 1 stop | 2024-01-08 20:53:20 | 0.0 | POINT (0.8983152841195217 0) |
22 | Sailing from node 1 to node 0 start | 2024-01-08 20:53:20 | 500000.0 | POINT (0.8983152841195217 0) |
23 | Sailing from node 1 to node 0 stop | 2024-01-10 00:40:00 | 600000.0 | POINT (0 0) |
24 | Loading from node 0 start | 2024-01-10 00:40:00 | 0.0 | POINT (0 0) |
25 | Loading from node 0 stop | 2024-01-10 03:26:40 | 0.0 | POINT (0 0) |
26 | Sailing from node 0 to node 1 start | 2024-01-10 03:26:40 | 600000.0 | POINT (0 0) |
27 | Sailing from node 0 to node 1 stop | 2024-01-11 07:13:20 | 700000.0 | POINT (0.8983152841195217 0) |
28 | Unloading to node 1 start | 2024-01-11 07:13:20 | 0.0 | POINT (0.8983152841195217 0) |
29 | Unloading to node 1 stop | 2024-01-11 10:00:00 | 0.0 | POINT (0.8983152841195217 0) |
30 | Sailing from node 1 to node 0 start | 2024-01-11 10:00:00 | 700000.0 | POINT (0.8983152841195217 0) |
31 | Sailing from node 1 to node 0 stop | 2024-01-12 13:46:40 | 800000.0 | POINT (0 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 | 00977eee-69e5-49c4-b962-d9a1da7d4c90 | Vessel | Loading from node 0 | POINT (0 0) | POINT (0 0) | 2024-01-01 00:00:00 | 2024-01-01 08:20:00 | 0.0 | 30000.0 |
1 | 00977eee-69e5-49c4-b962-d9a1da7d4c90 | Vessel | Sailing from node 0 to node 1 | POINT (0 0) | POINT (0.8983152841195217 0) | 2024-01-01 08:20:00 | 2024-01-02 12:06:40 | 100000.0 | 100000.0 |
2 | 00977eee-69e5-49c4-b962-d9a1da7d4c90 | Vessel | Unloading to node 1 | POINT (0.8983152841195217 0) | POINT (0.8983152841195217 0) | 2024-01-02 12:06:40 | 2024-01-02 20:26:40 | 0.0 | 30000.0 |
3 | 00977eee-69e5-49c4-b962-d9a1da7d4c90 | Vessel | Sailing from node 1 to node 0 | POINT (0.8983152841195217 0) | POINT (0 0) | 2024-01-02 20:26:40 | 2024-01-04 00:13:20 | 100000.0 | 100000.0 |
4 | 00977eee-69e5-49c4-b962-d9a1da7d4c90 | Vessel | Loading from node 0 | POINT (0 0) | POINT (0 0) | 2024-01-04 00:13:20 | 2024-01-04 08:33:20 | 0.0 | 30000.0 |
5 | 00977eee-69e5-49c4-b962-d9a1da7d4c90 | Vessel | Sailing from node 0 to node 1 | POINT (0 0) | POINT (0.8983152841195217 0) | 2024-01-04 08:33:20 | 2024-01-05 12:20:00 | 100000.0 | 100000.0 |
6 | 00977eee-69e5-49c4-b962-d9a1da7d4c90 | Vessel | Unloading to node 1 | POINT (0.8983152841195217 0) | POINT (0.8983152841195217 0) | 2024-01-05 12:20:00 | 2024-01-05 20:40:00 | 0.0 | 30000.0 |
7 | 00977eee-69e5-49c4-b962-d9a1da7d4c90 | Vessel | Sailing from node 1 to node 0 | POINT (0.8983152841195217 0) | POINT (0 0) | 2024-01-05 20:40:00 | 2024-01-07 00:26:40 | 100000.0 | 100000.0 |
8 | 00977eee-69e5-49c4-b962-d9a1da7d4c90 | Vessel | Loading from node 0 | POINT (0 0) | POINT (0 0) | 2024-01-07 00:26:40 | 2024-01-07 08:46:40 | 0.0 | 30000.0 |
9 | 00977eee-69e5-49c4-b962-d9a1da7d4c90 | Vessel | Sailing from node 0 to node 1 | POINT (0 0) | POINT (0.8983152841195217 0) | 2024-01-07 08:46:40 | 2024-01-08 12:33:20 | 100000.0 | 100000.0 |
10 | 00977eee-69e5-49c4-b962-d9a1da7d4c90 | Vessel | Unloading to node 1 | POINT (0.8983152841195217 0) | POINT (0.8983152841195217 0) | 2024-01-08 12:33:20 | 2024-01-08 20:53:20 | 0.0 | 30000.0 |
11 | 00977eee-69e5-49c4-b962-d9a1da7d4c90 | Vessel | Sailing from node 1 to node 0 | POINT (0.8983152841195217 0) | POINT (0 0) | 2024-01-08 20:53:20 | 2024-01-10 00:40:00 | 100000.0 | 100000.0 |
12 | 00977eee-69e5-49c4-b962-d9a1da7d4c90 | Vessel | Loading from node 0 | POINT (0 0) | POINT (0 0) | 2024-01-10 00:40:00 | 2024-01-10 03:26:40 | 0.0 | 10000.0 |
13 | 00977eee-69e5-49c4-b962-d9a1da7d4c90 | Vessel | Sailing from node 0 to node 1 | POINT (0 0) | POINT (0.8983152841195217 0) | 2024-01-10 03:26:40 | 2024-01-11 07:13:20 | 100000.0 | 100000.0 |
14 | 00977eee-69e5-49c4-b962-d9a1da7d4c90 | Vessel | Unloading to node 1 | POINT (0.8983152841195217 0) | POINT (0.8983152841195217 0) | 2024-01-11 07:13:20 | 2024-01-11 10:00:00 | 0.0 | 10000.0 |
15 | 00977eee-69e5-49c4-b962-d9a1da7d4c90 | Vessel | Sailing from node 1 to node 0 | POINT (0.8983152841195217 0) | POINT (0 0) | 2024-01-11 10:00:00 | 2024-01-12 13:46:40 | 100000.0 | 100000.0 |
generate_vessel_gantt_chart(df_eventtable)
The inspection of the logbook data shows that the mission of the Vessel, resulted in four cycles where loading - sailing - unloading - sailing were executed in a sequence. The sites had a capacity of 100, and the Vessel a capacity of 30. It can be seen that the last cycle the loading and unloading take less time because the Vessel sailed only partially filled.