"""
Mixin classes related to vessel properties.
The following classes are provided:
- HasLength
- HasLoad
- Vesselproperties
"""
# package(s) for documentation, debugging, saving and loading
import logging
# math packages
import random
# spatial libraries
import networkx as nx
# package(s) related to the simulation
import simpy
# use OpenCLSim objects for core objects (identifiable is imported for later use)
from openclsim.core import SimpyObject
import opentnsim.graph.mixins
# get logger
logger = logging.getLogger(__name__)
[docs]
class HasLength(SimpyObject):
"""Mixin class: Something with a length. The length is modelled as a storage capacity
Parameters
-----------
length: float
length that can be requested
remaining_length: float, default=0
length that is still available at the beginning of the simulation.
args, kwargs:
passed to SimpyObject. Must at least contain parameter env: simpy.Environment.
Attributes
-----------
length: simpy.Container
the container that is used to limit the length that can be requested.
pos_length: simpy.Container
the container that is used to limit the length that can be requested.
"""
def __init__(self, length: float, remaining_length: float = 0, *args, **kwargs):
super().__init__(*args, **kwargs)
"""Initialization"""
self.length = simpy.Container(self.env, capacity=length, init=remaining_length)
self.pos_length = simpy.Container(self.env, capacity=length, init=remaining_length)
[docs]
class HasLoad:
"""Mixin class with load dependent height (H) and draught (T). The filling
degree (filling_degree: fraction) will interpolate between empty and full
height and draught.
Parameters
----------
H_e: float
height of the vessel when empty
H_f: float
height of the vessel when full
T_e: float
draught of the vessel when empty
T_f: float
draught of the vessel when full
filling_degree: float, default=0
fraction of the vessel that is filled upon creation
"""
def __init__(self, H_e, H_f, T_e, T_f, filling_degree=0, *args, **kwargs):
super().__init__(*args, **kwargs)
self.H_e = H_e
self.H_f = H_f
self.T_e = T_e
self.T_f = T_f
self.filling_degree = filling_degree
@property
def T(self):
"""Calculate current draught based on filling degree"""
T = self.filling_degree * (self.T_f - self.T_e) + self.T_e
return T
@property
def H(self):
"""Calculate current height based on filling degree"""
return self.filling_degree * (self.H_f - self.H_e) + self.H_e
[docs]
class VesselProperties:
"""Mixin class: Something that has vessel properties
This mixin is updated to better accommodate the ConsumesEnergy mixin
Parameters
----------
type: str
can contain info on vessel type (avv class, cemt_class or other)
B: float
vessel width
L: float
vessel length
h_min: float, optional
vessel minimum water depth.
The wather-depth can also be extracted from the network edges if they have the property ['Info']['GeneralDepth']
T: float, optional
actual draught.
If not given, it will be calculated based on the filling degree (use mixin HasLoad) or based on the payload and vessel type
safety_margin: float, optional
the water area above the waterway bed reserved to prevent ship grounding due to ship squatting during sailing
the value of safety margin depends on waterway bed material and ship types. For tanker vessel with rocky bed the safety
margin is recommended as 0.3 m based on Van Dorsser et al. The value setting for safety margin depends on the risk attitude
of the ship captain and shipping companies.
h_squat: boolean, optional
the water depth considering ship squatting while the ship moving (if False, squat is disabled, if True, squat is calculated)
payload: float, optional
cargo load [ton], the actual draught can be determined by knowing payload based on van Dorsser et al's method.
(https://www.researchgate.net/publication/344340126_The_effect_of_low_water_on_loading_capacity_of_inland_ships)
vessel_type: str, optional
vessel_type: vessel type can be selected from "Container","Dry_SH","Dry_DH","Barge","Tanker".
("Dry_SH" means dry bulk single hull, "Dry_DH" means dry bulk double hull),
based on van Dorsser et al's paper.
(https://www.researchgate.net/publication/344340126_The_effect_of_low_water_on_loading_capacity_of_inland_ships)
renewable_fuel_mass: float, optional
renewable fuel mass on board [kg]
renewable_fuel_volume: float, optional
renewable fuel volume on board [m3]
renewable_fuel_required_space: float, optional
renewable fuel required storage space (consider packaging factor) on board [m3]
"""
def __init__(
self,
type,
B,
L,
h_min=None,
T=None,
safety_margin=None,
h_squat=None,
payload=None,
vessel_type=None,
renewable_fuel_mass=None,
renewable_fuel_volume=None,
renewable_fuel_required_space=None,
*args,
**kwargs
):
super().__init__(*args, **kwargs)
"""Initialization
"""
self.type = type
self.B = B
self.L = L
# hidden because these can also computed on the fly
self._T = T
self._h_min = h_min
# alternative options
self.safety_margin = safety_margin
self.h_squat = h_squat
self.payload = payload
self.vessel_type = vessel_type
self.renewable_fuel_mass = renewable_fuel_mass
self.renewable_fuel_volume = renewable_fuel_volume
self.renewable_fuel_required_space = renewable_fuel_required_space
# for lock module: #TODO: op een andere manier toevoegen?
self.bound = "inbound"
@property
def T(self):
"""Compute the actual draught.
This will default to using the draught passed by the constructor. If it is None it will try to find one in the super class.
"""
if self._T is not None:
# if we were passed a T value, use tha one
T = self._T
elif self.T_f is not None and self.T_e is not None:
# base draught on filling degree
T = self.filling_degree * (self.T_f - self.T_e) + self.T_e
elif self.payload is not None and self.vessel_type is not None:
T = opentnsim.strategy.Payload2T(
self,
Payload_strategy=self.payload,
vessel_type=self.vessel_type,
bounds=(0, 40),
) # this need to be tested
# todo: for later possibly include Payload2T
return T
@property
def h_min(self):
"""get the minimum water depth. if not given, h_min is the minimal water depth of the graph.."""
if self._h_min is not None:
h_min = self._h_min
else:
h_min = opentnsim.graph.mixins.get_minimum_depth(graph=self.graph, route=self.route)
return h_min
[docs]
def get_route(
self,
origin,
destination,
graph=None,
minWidth=None,
minHeight=None,
minDepth=None,
randomSeed=4,
):
"""Calculate a path based on vessel restrictions
restrictions are only applied if the graph has the attributes ['Width', 'Height', 'Depth']
Parameters
----------
origin: str
ID of the starting node
destination: str
ID of the destination node
graph: networkx.Graph, default = self.graph
The graph to use for the pathfinding
minWidth: float, default = 1.1 * self.B
Minimum width of the path
minHeight: float, default = 1.1 * self.H
Minimum height of the path
minDepth: float, default = 1.1 * self.T
Minimum depth of the path
randomSeed: int, default = 4
Seed for the random number generator
"""
graph = graph if graph else self.graph
minWidth = minWidth if minWidth else 1.1 * self.B
minHeight = minHeight if minHeight else 1.1 * self.H
minDepth = minDepth if minDepth else 1.1 * self.T
# Check if information on restrictions is added to the edges
random.seed(randomSeed)
edge = random.choice(list(graph.edges(data=True)))
edge_attrs = list(edge[2].keys())
# IMPROVE THIS TO CHECK ALL EDGES AND COMBINATIONS OF RESTRICTIONS
if all(item in edge_attrs for item in ["Width", "Height", "Depth"]):
edges = []
nodes = []
for edge in graph.edges(data=True):
if edge[2]["Width"] >= minWidth and edge[2]["Height"] >= minHeight and edge[2]["Depth"] >= minDepth:
edges.append(edge)
nodes.append(graph.nodes[edge[0]])
nodes.append(graph.nodes[edge[1]])
subGraph = graph.__class__()
for node in nodes:
subGraph.add_node(
node["name"],
name=node["name"],
geometry=node["geometry"],
position=(node["geometry"].x, node["geometry"].y),
)
for edge in edges:
subGraph.add_edge(edge[0], edge[1], attr_dict=edge[2])
try:
return nx.dijkstra_path(subGraph, origin, destination)
# return nx.bidirectional_dijkstra(subGraph, origin, destination)
except nx.NetworkXNoPath:
raise ValueError("No path was found with the given boundary conditions.")
# If not, return shortest path
else:
return nx.dijkstra_path(graph, origin, destination)