elements.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. import numpy as np
  2. from pypdevs.DEVS import AtomicDEVS, CoupledDEVS
  3. from pypdevs.infinity import INFINITY
  4. from dataclasses import dataclass, field
  5. import random
  6. import pandas as pd
  7. from de2.routing import get_graph, get_closest_vertex
  8. TUGS = pd.read_excel("20230405_Tugs.xlsx", dtype={"MMSI": str, "NAME": str, "category": str})
  9. @dataclass
  10. class Footprint:
  11. usage_time: float = 0.0
  12. idle_time: float = 0.0
  13. idle_since: float = 0.0
  14. sailing_time: float = 0.0
  15. tugging_time: float = 0.0
  16. fuel_usage: float = 0.0
  17. velocity_total: float = 0.0
  18. tasks: int = 0
  19. @staticmethod
  20. def get_fuel_efficiency(velocity):
  21. """
  22. Computes how efficient fuel is being used at a certain velocity.
  23. Function based on the figure from Pieter (see figures/fuel.jpg) and an
  24. online curve fitter (https://mycurvefit.com/).
  25. """
  26. return 32.89402 + (1.114886 - 32.89402) / (1 + (velocity / 12.20228) ** 6.449555)
  27. @staticmethod
  28. def get_fuel(distance, velocity):
  29. # TODO: find the right value here
  30. fuel = 5 # litres/meter for 100% efficient fuel usage
  31. efficiency = Footprint.get_fuel_efficiency(velocity)
  32. return distance * fuel * efficiency
  33. @dataclass
  34. class Vessel:
  35. mmsi: str
  36. velocity: float = 0.0
  37. source: tuple = None
  38. target: tuple = None
  39. task: str = None
  40. distance_left: float = 0.0
  41. total_distance: float = 0.0
  42. time_until_departure: float = 0.0
  43. footprint: Footprint = field(default_factory=Footprint)
  44. name: str = ""
  45. category: str = "weak"
  46. def tuple(self):
  47. return self.mmsi, self.name,\
  48. self.source, self.target,\
  49. self.task, self.total_distance, self.distance_left, self.velocity
  50. class Pool(AtomicDEVS):
  51. def __init__(self, name):
  52. super(Pool, self).__init__(name)
  53. self.state = {
  54. "waiting": {},
  55. "should_exit": [],
  56. "time": 0.0
  57. }
  58. self.req_in = self.addInPort("req_in")
  59. self.vessel_in = self.addInPort("vessel_in")
  60. self.vessel_out = self.addOutPort("vessel_out")
  61. # prefill with all vessels of the simulation
  62. for mmsi in TUGS["MMSI"]:
  63. if isinstance(mmsi, str):
  64. self.state["waiting"][mmsi] = Vessel(mmsi)
  65. def extTransition(self, inputs):
  66. self.state["time"] += self.elapsed
  67. if self.req_in in inputs:
  68. for request in inputs[self.req_in]:
  69. if request["mmsi"] in self.state["waiting"]:
  70. vessel = self.state["waiting"][request["mmsi"]]
  71. else:
  72. print("VESSEL %s DOES NOT EXIST IN POOL" % str(request["mmsi"]))
  73. # print("\t", request)
  74. vessel = Vessel(request["mmsi"])
  75. vessel.footprint.idle_time += self.state["time"] - vessel.footprint.idle_since
  76. # vessel.time_until_departure = (request["end"] - request["start"]) / 1000
  77. # vessel.velocity = request["distance"] / vessel.time_until_departure
  78. # vessel.distance_left = request["distance"]
  79. # vessel.total_distance = request["distance"]
  80. # vessel.source = (request["source_lon"], request["source_lat"])
  81. # vessel.target = (request["target_lon"], request["target_lat"])
  82. # vessel.location = request["location"]
  83. vessel.velocity = request["velocity"]
  84. if vessel.velocity > 0: # requests for idling should be ignored!
  85. vessel.distance_left = request["distance"]
  86. vessel.total_distance = vessel.distance_left
  87. # vessel.time_until_departure = vessel.distance_left / vessel.velocity
  88. vessel.source = request["source_lon"], request["source_lat"]
  89. vessel.target = request["target_lon"], request["target_lat"]
  90. vessel.task = request["task"]
  91. vessel.name = request["name"]
  92. vessel.category = request["category"]
  93. self.state["should_exit"].append(vessel)
  94. if vessel.mmsi in self.state["waiting"]:
  95. del self.state["waiting"][vessel.mmsi]
  96. if self.vessel_in in inputs:
  97. for vessel in inputs[self.vessel_in]:
  98. if vessel.mmsi in self.state["waiting"]:
  99. raise ValueError("Cannot create duplicate vessels (loc = %s; mmsi = %s; d = %f)" %
  100. (str(vessel.location), str(vessel.mmsi), vessel.total_distance))
  101. else:
  102. vessel.source = vessel.target
  103. vessel.target = None
  104. if vessel.velocity > 0:
  105. time = vessel.total_distance / vessel.velocity
  106. # vessel.footprint.fuel_usage += Footprint.get_fuel(vessel.total_distance, vessel.velocity)
  107. vessel.footprint.usage_time += time
  108. if vessel.task == "sailing":
  109. vessel.footprint.sailing_time += time
  110. else:
  111. vessel.footprint.tugging_time += time
  112. vessel.footprint.velocity_total += vessel.velocity
  113. vessel.footprint.tasks += 1
  114. vessel.footprint.idle_since = self.state["time"]
  115. self.state["waiting"][vessel.mmsi] = vessel
  116. return self.state
  117. def timeAdvance(self):
  118. if len(self.state["should_exit"]) > 0:
  119. return 0.0
  120. return INFINITY
  121. def outputFnc(self):
  122. if len(self.state["should_exit"]) > 0:
  123. return {
  124. self.vessel_out: [self.state["should_exit"][0]]
  125. }
  126. return {}
  127. def intTransition(self):
  128. self.state["time"] += self.timeAdvance()
  129. if len(self.state["should_exit"]) > 0:
  130. self.state["should_exit"].pop(0)
  131. return self.state
  132. class Sailer(AtomicDEVS):
  133. def __init__(self, name, v_tugging, v_sailing, v_max):
  134. super(Sailer, self).__init__(name)
  135. self.v_tugging = v_tugging
  136. self.v_sailing = v_sailing
  137. self.v_max = v_max
  138. self.state = {
  139. "vessels": [],
  140. "time": 0.0
  141. }
  142. self.vessel_in = self.addInPort("vessel_in")
  143. self.update = self.addInPort("update")
  144. self.vessel_out = self.addOutPort("vessel_out")
  145. def extTransition(self, inputs):
  146. self.state["time"] += self.elapsed
  147. self.update_vessels(self.elapsed)
  148. if self.vessel_in in inputs:
  149. for vessel in inputs[self.vessel_in]:
  150. if vessel.task == "tugging" and self.v_tugging is not None:
  151. vessel.velocity = -1
  152. while vessel.velocity <= 0 or vessel.velocity > self.v_max:
  153. vessel.velocity = random.normalvariate(*self.v_tugging)
  154. elif vessel.task == "sailing" and self.v_sailing is not None:
  155. vessel.velocity = -1
  156. while vessel.velocity <= 0 or vessel.velocity > self.v_max:
  157. vessel.velocity = random.normalvariate(*self.v_sailing)
  158. vessel.time_until_departure = vessel.distance_left / vessel.velocity
  159. self.state["vessels"].append(vessel)
  160. self.state["vessels"].sort(key=lambda v: v.time_until_departure)
  161. return self.state
  162. def update_vessels(self, elapsed):
  163. for vessel in self.state["vessels"]:
  164. x = vessel.velocity * elapsed
  165. vessel.distance_left = max(0.0, vessel.distance_left - x)
  166. vessel.time_until_departure = round(max(0.0, vessel.time_until_departure - elapsed), 6)
  167. def timeAdvance(self):
  168. if len(self.state["vessels"]) > 0:
  169. v = self.state["vessels"][0]
  170. return v.time_until_departure
  171. return INFINITY
  172. def outputFnc(self):
  173. if len(self.state["vessels"]) > 0:
  174. return {
  175. self.vessel_out: [self.state["vessels"][0]]
  176. }
  177. return {}
  178. def intTransition(self):
  179. elapsed = self.timeAdvance()
  180. self.state["time"] += elapsed
  181. self.state["vessels"].pop(0)
  182. self.update_vessels(elapsed)
  183. if len(self.state["vessels"]) > 0:
  184. self.state["vessels"].sort(key=lambda v: v.time_until_departure)
  185. return self.state
  186. class Scheduler(AtomicDEVS):
  187. def __init__(self, name, ivef):
  188. super(Scheduler, self).__init__(name)
  189. self.ivef = pd.read_csv(ivef, usecols=["mmsi", "start", "ETA", "source", "target", "task"], dtype={"mmsi": str})
  190. # self.ivef = self.ivef[self.ivef["mmsi"] == "205501790"]
  191. self.ivef.sort_values(by=["start"])
  192. self.starting_time = self.ivef.iloc[0]["start"]
  193. # self.ivef["start"] -= self.starting_time
  194. # self.ivef["end"] -= self.starting_time
  195. self.state = {
  196. "index": 0,
  197. "time": 0.0
  198. }
  199. self.reqs = self.addOutPort("reqs")
  200. def timeAdvance(self):
  201. if self.state["index"] < len(self.ivef):
  202. start = self.ivef.iloc[self.state["index"]]["start"] - self.starting_time
  203. return round(start / 1000 - self.state["time"], 6)
  204. return INFINITY
  205. def intTransition(self):
  206. self.state["time"] += self.timeAdvance()
  207. self.state["index"] += 1
  208. return self.state
  209. def outputFnc(self):
  210. if self.state["index"] < len(self.ivef):
  211. return {
  212. self.reqs: [self.ivef.iloc[self.state["index"]]]
  213. }
  214. return {}
  215. class RoutePlanner(AtomicDEVS):
  216. def __init__(self, name):
  217. super(RoutePlanner, self).__init__(name)
  218. self.graph = get_graph()
  219. self.state = {
  220. "request": []
  221. }
  222. self.req_in = self.addInPort("req_in")
  223. self.req_out = self.addOutPort("req_out")
  224. def timeAdvance(self):
  225. if len(self.state["request"]) == 0:
  226. return INFINITY
  227. return 0.0
  228. def extTransition(self, inputs):
  229. if self.req_in in inputs:
  230. for request in inputs[self.req_in]:
  231. tug = TUGS[TUGS["MMSI"] == str(request["mmsi"])].iloc[0]
  232. request["name"] = tug["NAME"]
  233. request["category"] = tug["category"]
  234. request["source_lon"], request["source_lat"] = eval(request["source"])
  235. request["target_lon"], request["target_lat"] = eval(request["target"])
  236. src_vertex, _ = get_closest_vertex(self.graph, request["source_lon"], request["source_lat"])
  237. tgt_vertex, _ = get_closest_vertex(self.graph, request["target_lon"], request["target_lat"])
  238. request["distance"] = self.graph.distances([src_vertex.index], [tgt_vertex.index], weights="distance", mode="all")[0][0]
  239. # TODO: check this value with PoAB
  240. # TODO: make this a distribution instead?
  241. # request["velocity"] = 6.17 # about 12 knots
  242. request["velocity"] = request["distance"] / ((request["ETA"] - request["start"]) / 1000)
  243. # UNUSED!
  244. # request["end"] = request["start"] + request["distance"] / request["velocity"]
  245. self.state["request"].append(request)
  246. return self.state
  247. def outputFnc(self):
  248. if len(self.state["request"]) == 0:
  249. return {}
  250. return { self.req_out: [self.state["request"][0]] }
  251. def intTransition(self):
  252. if len(self.state["request"]) > 0:
  253. self.state["request"].pop(0)
  254. return self.state
  255. class Clock(AtomicDEVS):
  256. def __init__(self, name, interval=1):
  257. super(Clock, self).__init__(name)
  258. self.interval = interval
  259. self.state = {
  260. "time": 0.0
  261. }
  262. self.outp = self.addOutPort("outp")
  263. def timeAdvance(self):
  264. return self.interval
  265. def outputFnc(self):
  266. return {
  267. self.outp: [True]
  268. }
  269. def intTransition(self):
  270. self.state["time"] += self.timeAdvance()
  271. return self.state
  272. class Port(CoupledDEVS):
  273. def __init__(self, name, ivef, v_tugging=None, v_sailing=None, v_max=None):
  274. super(Port, self).__init__(name)
  275. self.clock = self.addSubModel(Clock("clock"))
  276. self.scheduler = self.addSubModel(Scheduler("scheduler", ivef))
  277. self.planner = self.addSubModel(RoutePlanner("planner"))
  278. # self.tracer = self.addSubModel(Tracer("tracer", ivef))
  279. self.pool = self.addSubModel(Pool("pool"))
  280. self.sailer = self.addSubModel(Sailer("sailer", v_tugging, v_sailing, v_max))
  281. # self.connectPorts(self.tracer.reqs, self.pool.req_in)
  282. self.connectPorts(self.scheduler.reqs, self.planner.req_in)
  283. self.connectPorts(self.planner.req_out, self.pool.req_in)
  284. self.connectPorts(self.pool.vessel_out, self.sailer.vessel_in)
  285. self.connectPorts(self.sailer.vessel_out, self.pool.vessel_in)
  286. self.connectPorts(self.clock.outp, self.sailer.update)
  287. def select(self, imm_children):
  288. for child in imm_children:
  289. if isinstance(child, Sailer):
  290. return child
  291. for child in imm_children:
  292. if isinstance(child, Pool):
  293. return child
  294. return imm_children[0]
  295. def print_statistics(self):
  296. # Costs // Dummy data for now
  297. C_Tug_S = { # euro/hour
  298. "strong": 150,
  299. "average": 100,
  300. "weak": 50
  301. }
  302. C_Tug_P = { # euro/hour
  303. "strong": 200,
  304. "average": 133,
  305. "weak": 67
  306. }
  307. C_Tug_F = { # euro/day
  308. "strong": 1500,
  309. "average": 1000,
  310. "weak": 500
  311. }
  312. C_Tug_W = { # euro/hour
  313. "strong": 100,
  314. "average": 67,
  315. "weak": 33
  316. }
  317. S1 = {
  318. "strong": 0.0,
  319. "average": 0.0,
  320. "weak": 0.0
  321. }
  322. S2 = {
  323. "strong": 0.0,
  324. "average": 0.0,
  325. "weak": 0.0
  326. }
  327. S3 = {
  328. "strong": 0.0,
  329. "average": 0.0,
  330. "weak": 0.0
  331. }
  332. S4 = {
  333. "strong": 0.0,
  334. "average": 0.0,
  335. "weak": 0.0
  336. }
  337. for vessel in list(self.pool.state["waiting"].values()) + self.sailer.state["vessels"]:
  338. T_Tug_S = vessel.footprint.sailing_time / 3600
  339. T_Tug_P = vessel.footprint.tugging_time / 3600
  340. T_Tug_W = vessel.footprint.idle_time / 3600
  341. S1[vessel.category] += C_Tug_S[vessel.category] * T_Tug_S
  342. S2[vessel.category] += C_Tug_P[vessel.category] * T_Tug_P
  343. S3[vessel.category] += C_Tug_W[vessel.category] * T_Tug_W
  344. S4[vessel.category] += C_Tug_F[vessel.category] * 31
  345. for cat in ["strong", "average", "weak"]:
  346. print("\ncategory:", cat)
  347. print("OVERALL COST: %f euros" % (S1[cat] + S2[cat] + S3[cat] + S4[cat]))
  348. print("Travelling: %f euros" % S1[cat])
  349. print("Tugging: %f euros" % S2[cat])
  350. print("Waiting: %f euros" % S3[cat])
  351. print("Fixed: %f euros" % S4[cat])
  352. # print("AVERAGE USAGES:")
  353. # S = 0
  354. # for vessel in list(self.pool.state["waiting"].values()) + self.sailer.state["vessels"]:
  355. # if vessel.footprint.usage_time > 0:
  356. # print("\t%s: %f m/s" % (vessel.mmsi, vessel.footprint.velocity_total / vessel.footprint.tasks))
  357. # S += vessel.footprint.usage_time
  358. # cost_per_hour = 100
  359. # print("-------------")
  360. # print("OVERALL: %f euros" % (S / 3600 * cost_per_hour))