Basic trip over a single edge#
In this notebook, we set up a basic simulation where one vessel moves over a 1D network path. The example aims to provide a basic understanding of some key OpenTNSim core functions and shows what a basic OpenTNSim model run looks like.
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
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
),
{}
)
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 0
# define nodes with their geographic coordinates
coords = {
"0": (lon0, lat0),
"1": (lon1, lat1),
}
# create list of edges
edges = [("0", "1"), ("1", "0")] # 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
GeneralDepth = 10 # m
Current = 1 # m/s
# add graph to environment
FG.edges['0', '1']['Info'] = {'GeneralDepth': GeneralDepth, 'Current': -Current} # from '0' to '1' you sail against the current
FG.edges['1', '0']['Info'] = {'GeneralDepth': GeneralDepth, 'Current': Current} # from '1' to '0' you sail with the current
env.graph = FG
# create vessel from a dict
path = nx.dijkstra_path(env.graph, "0", "1")
path.append('0')
data_vessel = {
"env": env, # needed for simpy simulation
"name": "Vessel", # required by Identifiable
"geometry": env.graph.nodes[path[0]]['geometry'], # required by Locatable
"route": path, # required by Routeable
"v": 3, # required by Movable, 1 m/s to check if the distance is covered in the expected time
} #
# 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)
print("'{}' logbook data:".format(vessel.name))
print('')
display(df)
trip_distance = opentnsim.graph.calculate_distance_along_path(FG, vessel.route)
trip_duration = datetime.timedelta.total_seconds(vessel.logbook[-1]['Timestamp'] - vessel.logbook[0]['Timestamp'])
print("'{}' travelled a distance of {:.1f} meters".format(vessel.name, trip_distance))
print("'{}' took {:.1f} seconds to arrive at its destination".format(vessel.name, trip_duration))
print("'{}' travelled at an average speed of {:.1f} meters per second".format(vessel.name, trip_distance/trip_duration))
print('')
print('')
'Vessel' logbook data:
Message | Timestamp | Value | Geometry | |
---|---|---|---|---|
0 | Sailing from node 0 to node 1 start | 2024-01-01 00:00:00 | 0.0 | POINT (0 0) |
1 | Sailing from node 0 to node 1 stop | 2024-01-01 13:53:20 | 100000.0 | POINT (0.8983152841195217 0) |
2 | Sailing from node 1 to node 0 start | 2024-01-01 13:53:20 | 100000.0 | POINT (0.8983152841195217 0) |
3 | Sailing from node 1 to node 0 stop | 2024-01-01 20:50:00 | 200000.0 | POINT (0 0) |
'Vessel' travelled a distance of 200000.0 meters
'Vessel' took 75000.0 seconds to arrive at its destination
'Vessel' travelled at an average speed of 2.7 meters per second
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 | 6aae1c30-c67c-4aab-b6fe-924d7ce58119 | Vessel | Sailing from node 0 to node 1 | POINT (0 0) | POINT (0.8983152841195217 0) | 2024-01-01 00:00:00 | 2024-01-01 13:53:20 | 100000.0 | 50000.0 |
1 | 6aae1c30-c67c-4aab-b6fe-924d7ce58119 | Vessel | Sailing from node 1 to node 0 | POINT (0.8983152841195217 0) | POINT (0 0) | 2024-01-01 13:53:20 | 2024-01-01 20:50:00 | 100000.0 | 25000.0 |
generate_vessel_gantt_chart(df_eventtable)