Simulating traffic around a lock (with hydrodynamics)#
In this notebook, we simulate a lock on a network which two opposingly directed vessels have to pass. We add a pre-coded complex lock object on the graph. Vessels are locked together if they can fit inside the lock, and arrive within the clustering time window. Changing water levels are implemented, which influences the duration of the levelling.
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.core.logutils import logbook2eventtable
from opentnsim.core.plotutils import generate_vessel_gantt_chart
from scipy.stats import norm, uniform, expon
# 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
from opentnsim.graph import mixins as graph_module
# package(s) needed for inspecting the output
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# packege(s) needed to create hydrodynamic data
import xarray as xr
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('-1',geometry=transform(wgs84eqd_to_wgs84rad,Point(-350600,0)))
graph.add_node('0',geometry=transform(wgs84eqd_to_wgs84rad,Point(-5000,0)))
graph.add_node('1',geometry=transform(wgs84eqd_to_wgs84rad,Point(5000,0)))
graph.add_node('+1',geometry=transform(wgs84eqd_to_wgs84rad,Point(350600,0)))
# add edges
graph.add_edge('-1','0', weight=1)
graph.add_edge('0','-1', weight=1)
graph.add_edge('0','1', weight=1)
graph.add_edge('1','0', weight=1)
graph.add_edge('1','+1', weight=1)
graph.add_edge('+1','1', weight=1)
graph_module.plot_graph(graph)
3. Run simulation#
def generate_vessel(
env,
name,
start_node,
end_node,
arrival_time,
vessel_speed=4,
vessel_length=100,
vessel_beam=20,
vessel_draft=10,
vessel_type="tanker"):
"""
Creates and returns a Vessel object with a computed route through the environment graph.
Parameters:
----------
env : Environment
The simulation environment containing the graph and other context.
name : str
Human readabile identifier for the vessel.
start_node : str or int
The starting node in the graph (converted to string).
end_node : str or int
The destination node in the graph (converted to string).
arrival_time : pd.Timestamp
The scheduled arrival time of the vessel at the start node.
vessel_speed : float, optional
Speed of the vessel in knots or simulation units (default is 4).
vessel_length : float, optional
Length of the vessel in meters (default is 100).
vessel_beam : float, optional
Beam (width) of the vessel in meters (default is 20).
vessel_draft : float, optional
Draught (depth below waterline) of the vessel in meters (default is 10).
vessel_type : str, optional
Type of vessel (e.g., "tanker", "cargo", "container") (default is "tanker").
Returns:
-------
Vessel or None
A Vessel object initialized with the given parameters and route.
Returns None if no valid path exists between start_node and end_node.
"""
# Ensure nodes are strings
start_node = str(start_node)
end_node = str(end_node)
try:
route = nx.dijkstra_path(env.graph, start_node, end_node)
except nx.NetworkXNoPath:
print(f"⚠️ No path from {start_node} to {end_node}. Vessel {name} not created.")
return None
geometry = env.graph.nodes[start_node]['geometry']
data_vessel = {
"env": env,
"name": name,
"geometry": geometry,
"route": route,
"v": vessel_speed,
"L": vessel_length,
"B": vessel_beam,
"T": vessel_draft,
"type": vessel_type,
"arrival_time": arrival_time,
}
vessel = Vessel(**data_vessel)
return vessel
def generate_vessels_with_distributions(
env,
num_vessels,
start_time,
arrival_dist_up=None,
arrival_dist_down=None,
seed_up=None,
seed_down=None):
"""
Generates a list of vessels with interarrival times drawn from specified distributions
for upward and downward directions. Supports independent seeding for reproducibility.
Parameters
----------
env : Environment
The simulation environment containing the graph and vessel context.
num_vessels : int
Total number of vessels to generate. Vessels alternate between up and down directions.
start_time : pd.Timestamp
The initial timestamp from which vessel arrivals begin.
arrival_dist_up : callable, optional
A function returning interarrival times (in minutes) for upward-moving vessels.
If None, defaults to an exponential distribution with mean 20 minutes.
arrival_dist_down : callable, optional
A function returning interarrival times (in minutes) for downward-moving vessels.
If None, defaults to an exponential distribution with mean 20 minutes.
seed_up : int or None, optional
Seed for the random number generator used in upward direction.
seed_down : int or None, optional
Seed for the random number generator used in downward direction.
Returns
-------
list of Vessel
A list of Vessel objects with assigned routes and arrival times.
Vessels for which no valid path exists are skipped.
"""
vessels = []
# Create independent random generators
rng_up = np.random.default_rng(seed_up)
rng_down = np.random.default_rng(seed_down)
# Default to exponential distribution with mean 20 minutes
if arrival_dist_up is None:
arrival_dist_up = lambda: rng_up.exponential(scale=20)
if arrival_dist_down is None:
arrival_dist_down = lambda: rng_down.exponential(scale=20)
up_time = start_time
down_time = start_time
for i in range(num_vessels):
if i % 2 == 0:
# Upward direction: -1 → +1
start_node, end_node = "-1", "+1"
delta_minutes = arrival_dist_up()
arrival_time = up_time + pd.Timedelta(minutes=delta_minutes)
up_time = arrival_time
else:
# Downward direction: +1 → -1
start_node, end_node = "+1", "-1"
delta_minutes = arrival_dist_down()
arrival_time = down_time + pd.Timedelta(minutes=delta_minutes)
down_time = arrival_time
vessel = generate_vessel(
env=env,
name=f"Vessel {i + 1}",
start_node=start_node,
end_node=end_node,
arrival_time=arrival_time
)
if vessel:
vessels.append(vessel)
return vessels
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
# generate synthetic hydrodynamic data
hydrodynamic_data = xr.Dataset()
time = pd.date_range(start = simulation_start,
end = simulation_start+pd.Timedelta(days=7),
freq = pd.Timedelta(minutes=5))
static_water_level = np.zeros(len(time))
tidal_amplitude = 1.0
tidal_period = 12.5 #hours
tidal_water_level = [tidal_amplitude*np.sin(2*np.pi*(t-simulation_start).total_seconds()/(tidal_period*3600)) for t in time]
stations = ['0', '1']
water_level_data = xr.DataArray(data=[static_water_level,tidal_water_level],coords={'STATION':stations,'TIME':time})
hydrodynamic_data['Water level'] = water_level_data
# 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",
hydrodynamic_information = hydrodynamic_data)
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 = ['-1','+1'],
)
start_time = pd.Timestamp('2025-01-01 00:00:00')
# Example using numpy
# Create independent random generators
rng_up = np.random.default_rng(123)
rng_down = np.random.default_rng(456)
# Exponential distributions with different means (scale = mean)
arrival_dist_up = lambda: rng_up.exponential(scale=30) # mean 30 minutes
arrival_dist_down = lambda: rng_down.exponential(scale=15) # mean 15 minutes
vessels = generate_vessels_with_distributions(
env=env,
num_vessels=9,
start_time=start_time,
arrival_dist_up=arrival_dist_up,
arrival_dist_down=arrival_dist_down,
seed_up=123,
seed_down=456
)
for vessel in vessels:
env.process(mission(env, vessel))
env.run()
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 | [] | 400 | 50 | 2025-01-01 00:07:22.177207 | 2025-01-01 00:07:22.177207 | 2025-01-01 00:07:22.177207 | ... | 2025-01-01 00:20:02.177207 | 2025-01-01 00:20:02.177207 | 2025-01-01 00:20:02.177207 | 2025-01-01 00:20:02.177207 | 2025-01-01 00:20:02.177207 | 0.0 | 0.12533323356430426 | 0 days 00:00:00 | 0 days 00:00:00 | available |
| 1 | 1 | 0 | 1 | Lock | [<__main__.Vessel object at 0x7f8f8d8ad810>, <... | 0 | -30 | 2025-01-02 00:17:22.177207 | 2025-01-02 00:19:52.177207 | 2025-01-02 00:27:22.177207 | ... | 2025-01-02 00:58:06.301059352 | 2025-01-02 00:58:06.301059352 | 2025-01-02 01:08:09.281621116 | 2025-01-02 01:15:39.281621116 | 2025-01-02 01:10:09.281621116 | 0.0 | -0.0836778433323161 | 0 days 00:08:43.951065882 | 0 days 00:16:46.623747764 | unavailable |
| 2 | 0 | 1 | 0 | Lock | [<__main__.Vessel object at 0x7f8f8d879e80>, <... | 0 | -30 | 2025-01-02 01:13:09.281621116 | 2025-01-02 01:15:39.281621116 | 2025-01-02 01:23:09.281621116 | ... | 2025-01-02 02:02:08.050519938 | 2025-01-02 02:02:08.050519938 | 2025-01-02 02:12:11.031081702 | 2025-01-02 02:19:41.031081702 | 2025-01-02 02:14:11.031081702 | 0.0 | 0.44463517918492684 | 0 days 00:59:23.327242468 | 0 days 01:19:48.875246350 | unavailable |
| 3 | 1 | 0 | 1 | Lock | [] | 400 | 50 | 2025-01-02 02:14:11.031081702 | 2025-01-02 02:14:11.031081702 | 2025-01-02 02:14:11.031081702 | ... | 2025-01-02 02:42:21.031081702 | 2025-01-02 02:42:21.031081702 | 2025-01-02 02:42:21.031081702 | 2025-01-02 02:42:21.031081702 | 2025-01-02 02:42:21.031081702 | 0.0 | 0.6211477802783093 | 0 days 00:00:00 | 0 days 00:00:00 | unavailable |
| 4 | 0 | 1 | 0 | Lock | [<__main__.Vessel object at 0x7f8f8d8ae350>] | 300 | 30 | 2025-01-02 02:42:21.031081702 | 2025-01-02 02:44:51.031081702 | 2025-01-02 02:52:21.031081702 | ... | 2025-01-02 03:30:41.203868172 | 2025-01-02 03:30:41.203868172 | 2025-01-02 03:31:29.799980524 | 2025-01-02 03:38:59.799980524 | 2025-01-02 03:33:29.799980524 | 0.0 | 0.9372819894918916 | 0 days 01:51:23.011631702 | 0 days 01:51:23.011631702 | unavailable |
5 rows × 26 columns
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 | 8ce2029c-fa47-401c-b0b5-90524d2fe93a | 1 | 0 | NaN | 100 | 20 | 10 | 1 | 2025-01-01 00:07:22.177207 | 2025-01-01 00:07:22.177207 | ... | NaN | 2025-01-02 00:19:52.177207 | 2025-01-02 00:27:22.177207 | 2025-01-02 00:33:02.349993470 | 2025-01-02 00:58:06.301059352 | 2025-01-02 00:58:54.897171704 | 2025-01-02 01:06:24.897171704 | 2025-01-02 00:33:02.349993470 | 1.0 | 0 days 00:08:43.951065882 |
| 1 | 7d17a715-cd7c-444b-8000-5b452c1cb634 | 1 | 0 | Lock | 100 | 20 | 10 | 1 | 2025-01-01 00:13:40.580269 | 2025-01-01 00:13:40.580269 | ... | NaN | 2025-01-02 00:26:10.580269 | 2025-01-02 00:33:40.580269 | 2025-01-02 00:37:43.560830764 | 2025-01-02 00:58:29.108834646 | 2025-01-02 01:00:54.897171704 | 2025-01-02 01:08:24.897171704 | 2025-01-02 00:37:43.560830764 | 1.0 | 0 days 00:05:42.740228588 |
| 2 | 6dfef98e-eca6-40da-9f51-587bc773e5ac | 1 | 0 | Lock | 100 | 20 | 10 | 1 | 2025-01-01 00:17:13.532738 | 2025-01-01 00:17:13.532738 | ... | NaN | 2025-01-02 00:31:10.580269 | 2025-01-02 00:38:40.580269 | 2025-01-02 00:41:06.368606058 | 2025-01-02 01:00:29.108834646 | 2025-01-02 01:04:32.089396410 | 2025-01-02 01:12:02.089396410 | 2025-01-02 00:41:06.368606058 | 1.0 | 0 days 00:02:19.932453294 |
| 3 | 38494a88-7d27-4a8c-9a31-c1b85e1f3809 | 0 | 1 | NaN | 100 | 20 | 10 | 2 | 2025-01-01 00:17:54.550491 | 2025-01-01 00:17:54.550491 | ... | NaT | 2025-01-02 01:15:39.281621116 | 2025-01-02 01:23:09.281621116 | 2025-01-02 01:28:49.454407586 | 2025-01-02 02:02:08.050519938 | 2025-01-02 02:02:56.646632290 | 2025-01-02 02:10:26.646632290 | 2025-01-02 01:28:49.454407586 | 0.0 | 0 days 00:59:23.327242468 |
| 4 | b4eddd03-3a12-47bb-92fe-859844831413 | 0 | 1 | Lock | 100 | 20 | 10 | 2 | 2025-01-01 00:21:25.206401 | 2025-01-01 00:21:25.206401 | ... | NaT | 2025-01-02 01:21:19.454407586 | 2025-01-02 01:28:49.454407586 | 2025-01-02 01:32:52.434969350 | 2025-01-02 02:02:30.858295232 | 2025-01-02 02:04:56.646632290 | 2025-01-02 02:12:26.646632290 | 2025-01-02 01:32:52.434969350 | 0.0 | 0 days 00:16:02.740228588 |
| 5 | 147044f7-e87b-4615-92d0-6dfbbf15cdc2 | 1 | 0 | Lock | 100 | 20 | 10 | 1 | 2025-01-01 00:24:37.704947 | 2025-01-01 00:24:37.704947 | ... | NaN | 2025-01-02 00:37:07.704947 | 2025-01-02 00:44:37.704947 | 2025-01-02 00:45:26.301059352 | 2025-01-02 01:02:29.108834646 | 2025-01-02 01:08:09.281621116 | 2025-01-02 01:15:39.281621116 | 2025-01-02 00:45:26.301059352 | 1.0 | 0 days 00:00:00 |
| 6 | 780dfbe3-c7d1-4531-ad72-7422d1653ff9 | 0 | 1 | Lock | 100 | 20 | 10 | 2 | 2025-01-01 00:28:58.436860 | 2025-01-01 00:28:58.436860 | ... | NaN | 2025-01-02 01:26:19.454407586 | 2025-01-02 01:33:49.454407586 | 2025-01-02 01:36:15.242744644 | 2025-01-02 02:04:30.858295232 | 2025-01-02 02:08:33.838856996 | 2025-01-02 02:16:03.838856996 | 2025-01-02 01:36:15.242744644 | 0.0 | 0 days 00:04:22.807775294 |
| 7 | e754af16-217e-4730-ac95-8c6edd76527f | 0 | 1 | Lock | 100 | 20 | 10 | 2 | 2025-01-01 00:38:12.774662 | 2025-01-01 00:38:12.774662 | ... | NaN | 2025-01-02 01:31:19.454407586 | 2025-01-02 01:38:49.454407586 | 2025-01-02 01:39:38.050519938 | 2025-01-02 02:06:30.858295232 | 2025-01-02 02:12:11.031081702 | 2025-01-02 02:19:41.031081702 | 2025-01-02 01:39:38.050519938 | 0.0 | 0 days 00:00:00 |
| 8 | 102ff1c0-bf3a-4245-a1a9-4ec3e2bb80b4 | 0 | 1 | NaN | 100 | 20 | 10 | 4 | 2025-01-01 00:40:58.019450 | 2025-01-01 00:40:58.019450 | ... | NaN | 2025-01-02 02:44:51.031081702 | 2025-01-02 02:52:21.031081702 | 2025-01-02 02:58:01.203868172 | 2025-01-02 03:30:41.203868172 | 2025-01-02 03:31:29.799980524 | 2025-01-02 03:38:59.799980524 | 2025-01-02 02:58:01.203868172 | 0.0 | 0 days 01:51:23.011631702 |
9 rows × 22 columns
4. Inspect output#
# 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:07:22.177207 | {} | 0 |
| 1 | Lock doors closing stop | 2025-01-01 00:12:22.177207 | {} | 0 |
| 2 | Lock chamber converting start | 2025-01-01 00:12:22.177207 | {} | 0 |
| 3 | Lock chamber converting stop | 2025-01-01 00:15:02.177207 | {} | 1 |
| 4 | Lock doors opening start | 2025-01-01 00:15:02.177207 | {} | 1 |
| 5 | Lock doors opening stop | 2025-01-01 00:20:02.177207 | {} | 1 |
| 6 | Lock doors closing start | 2025-01-02 00:45:26.301059 | {} | 1 |
| 7 | Lock doors closing stop | 2025-01-02 00:50:26.301059 | {} | 1 |
| 8 | Lock chamber converting start | 2025-01-02 00:50:26.301059 | {} | 1 |
| 9 | Lock chamber converting stop | 2025-01-02 00:53:06.301059 | {} | 0 |
| 10 | Lock doors opening start | 2025-01-02 00:53:06.301059 | {} | 0 |
| 11 | Lock doors opening stop | 2025-01-02 00:58:06.301059 | {} | 0 |
| 12 | Lock doors closing start | 2025-01-02 01:39:38.050519 | {} | 0 |
| 13 | Lock doors closing stop | 2025-01-02 01:44:38.050519 | {} | 0 |
| 14 | Lock chamber converting start | 2025-01-02 01:44:38.050519 | {} | 0 |
| 15 | Lock chamber converting stop | 2025-01-02 01:57:08.050519 | {} | 1 |
| 16 | Lock doors opening start | 2025-01-02 01:57:08.050519 | {} | 1 |
| 17 | Lock doors opening stop | 2025-01-02 02:02:08.050519 | {} | 1 |
| 18 | Lock doors closing start | 2025-01-02 02:14:11.031082 | {} | 1 |
| 19 | Lock doors closing stop | 2025-01-02 02:19:11.031082 | {} | 1 |
| 20 | Lock chamber converting start | 2025-01-02 02:19:11.031082 | {} | 1 |
| 21 | Lock chamber converting stop | 2025-01-02 02:37:21.031082 | {} | 0 |
| 22 | Lock doors opening start | 2025-01-02 02:37:21.031082 | {} | 0 |
| 23 | Lock doors opening stop | 2025-01-02 02:42:21.031082 | {} | 0 |
| 24 | Lock doors closing start | 2025-01-02 02:58:01.203867 | {} | 0 |
| 25 | Lock doors closing stop | 2025-01-02 03:03:01.203867 | {} | 0 |
| 26 | Lock chamber converting start | 2025-01-02 03:03:01.203867 | {} | 0 |
| 27 | Lock chamber converting stop | 2025-01-02 03:25:41.203867 | {} | 1 |
| 28 | Lock doors opening start | 2025-01-02 03:25:41.203867 | {} | 1 |
| 29 | Lock doors opening stop | 2025-01-02 03:30:41.203867 | {} | 1 |
Gantt chart of event table#
df_eventtable = opentnsim.core.logutils.logbook2eventtable([*vessels, lock.lock_chamber])
fig = generate_vessel_gantt_chart(df_eventtable)
Time-distance diagram of vessels passing the lock and planning info#
def cm_to_pixels(cm):
return cm * 37.8 # Set figure height to 10 cmfig.update_layout(height=cm_to_pixels(10))
# We can plot the time-distance diagram
fig = lock.create_time_distance_plot(vessels = vessels,
xlimmin = -6050,
xlimmax = 6050,
ylimmin = pd.Timestamp('2025-01-01 22:00:00'),
ylimmax = pd.Timestamp('2025-01-02 09:00:00'),
method='Plotly')
fig.update_layout(height=cm_to_pixels(20))
Levelling times as a function of water levels#
fig,ax = plt.subplots(figsize=[12,8])
ax.plot(hydrodynamic_data["TIME"],hydrodynamic_data.sel({'STATION':'1'})["Water level"])
ax.plot(hydrodynamic_data["TIME"],lock.lock_chamber.water_level,color='k')
ax.set_xlim([pd.Timestamp('2025-01-02'),pd.Timestamp('2025-01-02 06:00')])
(np.float64(20090.0), np.float64(20090.25))
levelling_times = []
water_level_difference = []
stationA_index = np.where(np.array(list((hydrodynamic_data['STATION'].values))) == lock.start_node)[0][0]
stationB_index = np.where(np.array(list((hydrodynamic_data['STATION'].values))) == lock.end_node)[0][0]
H_A = hydrodynamic_data["Water level"][stationA_index].values.copy()
H_B = hydrodynamic_data["Water level"][stationB_index].values.copy()
for time in pd.date_range(pd.Timestamp('2025-01-01T00:00:00.000000000'),pd.Timestamp('2025-01-01T12:30:00.000000000'),freq=pd.Timedelta(minutes=5)):
time_index = np.absolute(hydrodynamic_data['TIME'][:].values - np.datetime64(time)).argmin()
wlev_A = H_A[time_index]
wlev_B = H_B[time_index]
wlev_dif = wlev_A - wlev_B
water_level_difference.append(wlev_dif)
levelling_times.append(lock.determine_levelling_time(time,direction=1,wlev_init=wlev_dif,prediction=True)[0])
plt.plot(water_level_difference,np.array(levelling_times)/60)
plt.xlabel('Water level difference [m]')
plt.ylabel('Levelling time [min]');