|
|
@@ -0,0 +1,260 @@
|
|
|
+import numpy as np
|
|
|
+from pypdevs.DEVS import AtomicDEVS, CoupledDEVS
|
|
|
+from pypdevs.infinity import INFINITY
|
|
|
+
|
|
|
+from dataclasses import dataclass, field
|
|
|
+import random
|
|
|
+import pandas as pd
|
|
|
+
|
|
|
+from de2.routing import get_graph, get_closest_vertex
|
|
|
+
|
|
|
+
|
|
|
+TUGS = pd.read_excel("20230405_Tugs.xlsx", dtype={"MMSI": str, "NAME": str, "category": str})
|
|
|
+
|
|
|
+
|
|
|
+@dataclass
|
|
|
+class Vessel:
|
|
|
+ mmsi: str
|
|
|
+ velocity: float = 0.0
|
|
|
+ source: tuple = None
|
|
|
+ target: tuple = None
|
|
|
+ task: str = None
|
|
|
+ distance_left: float = 0.0
|
|
|
+ total_distance: float = 0.0
|
|
|
+ time_until_departure: float = 0.0
|
|
|
+
|
|
|
+ start: float = 0
|
|
|
+ ETA: float = 0
|
|
|
+
|
|
|
+ name: str = ""
|
|
|
+
|
|
|
+ def tuple(self):
|
|
|
+ return self.mmsi, self.name,\
|
|
|
+ self.source, self.target,\
|
|
|
+ self.task, self.total_distance, self.distance_left, self.velocity
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def knots_to_mps(knots):
|
|
|
+ return knots * 1852. / 3600.
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def ecological():
|
|
|
+ return Vessel.knots_to_mps(6), Vessel.knots_to_mps(7)
|
|
|
+
|
|
|
+
|
|
|
+class Pool(AtomicDEVS):
|
|
|
+ def __init__(self, name):
|
|
|
+ super(Pool, self).__init__(name)
|
|
|
+
|
|
|
+ self.state = {
|
|
|
+ "waiting": {},
|
|
|
+ "should_exit": [],
|
|
|
+ "delayed": [],
|
|
|
+ "time": 0.0,
|
|
|
+ "errors": []
|
|
|
+ }
|
|
|
+
|
|
|
+ self.req_in = self.addInPort("req_in")
|
|
|
+ self.vessel_in = self.addInPort("vessel_in")
|
|
|
+ self.vessel_out = self.addOutPort("vessel_out")
|
|
|
+ self.error = self.addOutPort("error")
|
|
|
+
|
|
|
+ # prefill with all vessels of the simulation
|
|
|
+ for mmsi in TUGS["MMSI"]:
|
|
|
+ if isinstance(mmsi, str):
|
|
|
+ self.state["waiting"][mmsi] = Vessel(mmsi)
|
|
|
+
|
|
|
+ def request_vessel(self, vessel, request):
|
|
|
+ vessel.velocity = request["velocity"]
|
|
|
+ if vessel.velocity >= 0: # requests for idling should be ignored!
|
|
|
+ vessel.distance_left = request["distance"]
|
|
|
+ vessel.total_distance = vessel.distance_left
|
|
|
+ # vessel.time_until_departure = vessel.distance_left / vessel.velocity
|
|
|
+ vessel.source = request["source_lon"], request["source_lat"]
|
|
|
+ vessel.target = request["target_lon"], request["target_lat"]
|
|
|
+ vessel.task = request["task"]
|
|
|
+
|
|
|
+ vessel.start = request["start"]
|
|
|
+ vessel.ETA = request["ETA"]
|
|
|
+ vessel.name = request["name"]
|
|
|
+
|
|
|
+ self.state["should_exit"].append(vessel)
|
|
|
+ if vessel.mmsi in self.state["waiting"]:
|
|
|
+ del self.state["waiting"][vessel.mmsi]
|
|
|
+
|
|
|
+ def extTransition(self, inputs):
|
|
|
+ self.state["time"] += self.elapsed
|
|
|
+ if self.req_in in inputs:
|
|
|
+ for request in inputs[self.req_in]:
|
|
|
+ if request["mmsi"] in self.state["waiting"]:
|
|
|
+ vessel = self.state["waiting"][request["mmsi"]]
|
|
|
+ self.request_vessel(vessel, request)
|
|
|
+ else:
|
|
|
+ self.state["errors"].append("%4.4f Vessel %s does not exist in pool - delaying request" % (self.state["time"], str(request["mmsi"])))
|
|
|
+ self.state["delayed"].append(request)
|
|
|
+
|
|
|
+ if self.vessel_in in inputs:
|
|
|
+ for vessel in inputs[self.vessel_in]:
|
|
|
+ if vessel.mmsi in self.state["waiting"]:
|
|
|
+ self.state["errors"].append("[WARN] %4.4f Cannot create duplicate vessels (mmsi = %s; d = %f)" %
|
|
|
+ (self.state["time"], str(vessel.mmsi), vessel.total_distance))
|
|
|
+ else:
|
|
|
+ vessel.source = vessel.target
|
|
|
+ vessel.target = None
|
|
|
+
|
|
|
+ self.state["waiting"][vessel.mmsi] = vessel
|
|
|
+
|
|
|
+ for ix, r in enumerate(self.state["delayed"]):
|
|
|
+ if r["mmsi"] == vessel.mmsi:
|
|
|
+ self.request_vessel(vessel, r)
|
|
|
+ self.state["delayed"].pop(ix)
|
|
|
+ print("[INFO] %4.4f Delay for vessel %s cleared" % (self.state["time"], r["mmsi"]))
|
|
|
+ break
|
|
|
+ return self.state
|
|
|
+
|
|
|
+ def timeAdvance(self):
|
|
|
+ if len(self.state["should_exit"]) > 0 or len(self.state["errors"]) > 0:
|
|
|
+ return 0.0
|
|
|
+ return INFINITY
|
|
|
+
|
|
|
+ def outputFnc(self):
|
|
|
+ res = {}
|
|
|
+ if len(self.state["should_exit"]) > 0:
|
|
|
+ res[self.vessel_out] = [self.state["should_exit"][0]]
|
|
|
+ if len(self.state["errors"]) > 0:
|
|
|
+ res[self.error] = self.state["errors"]
|
|
|
+ return res
|
|
|
+
|
|
|
+ def intTransition(self):
|
|
|
+ self.state["time"] += self.timeAdvance()
|
|
|
+ if len(self.state["should_exit"]) > 0:
|
|
|
+ self.state["should_exit"].pop(0)
|
|
|
+ if len(self.state["errors"]) > 0:
|
|
|
+ self.state["errors"].clear()
|
|
|
+ return self.state
|
|
|
+
|
|
|
+class Sailer(AtomicDEVS):
|
|
|
+ def __init__(self, name):
|
|
|
+ super(Sailer, self).__init__(name)
|
|
|
+
|
|
|
+ self.state = {
|
|
|
+ "vessels": [],
|
|
|
+ "time": 0.0
|
|
|
+ }
|
|
|
+
|
|
|
+ self.vessel_in = self.addInPort("vessel_in")
|
|
|
+ self.vessel_out = self.addOutPort("vessel_out")
|
|
|
+
|
|
|
+ def extTransition(self, inputs):
|
|
|
+ self.state["time"] += self.elapsed
|
|
|
+ self.update_vessels(self.elapsed)
|
|
|
+ if self.vessel_in in inputs:
|
|
|
+ for vessel in inputs[self.vessel_in]:
|
|
|
+ if vessel.distance_left == 0.0:
|
|
|
+ vessel.time_until_departure = 0.0
|
|
|
+ else:
|
|
|
+ vessel.time_until_departure = vessel.distance_left / vessel.velocity
|
|
|
+ self.state["vessels"].append(vessel)
|
|
|
+ self.state["vessels"].sort(key=lambda v: v.time_until_departure)
|
|
|
+ return self.state
|
|
|
+
|
|
|
+ def update_vessels(self, elapsed):
|
|
|
+ for vessel in self.state["vessels"]:
|
|
|
+ x = vessel.velocity * elapsed
|
|
|
+ vessel.distance_left = max(0.0, vessel.distance_left - x)
|
|
|
+ vessel.time_until_departure = round(max(0.0, vessel.time_until_departure - elapsed), 6)
|
|
|
+
|
|
|
+ def timeAdvance(self):
|
|
|
+ if len(self.state["vessels"]) > 0:
|
|
|
+ v = self.state["vessels"][0]
|
|
|
+ return v.time_until_departure
|
|
|
+ return INFINITY
|
|
|
+
|
|
|
+ def outputFnc(self):
|
|
|
+ if len(self.state["vessels"]) > 0:
|
|
|
+ return {
|
|
|
+ self.vessel_out: [self.state["vessels"][0]]
|
|
|
+ }
|
|
|
+ return {}
|
|
|
+
|
|
|
+ def intTransition(self):
|
|
|
+ elapsed = self.timeAdvance()
|
|
|
+ self.state["time"] += elapsed
|
|
|
+ self.state["vessels"].pop(0)
|
|
|
+ self.update_vessels(elapsed)
|
|
|
+ if len(self.state["vessels"]) > 0:
|
|
|
+ self.state["vessels"].sort(key=lambda v: v.time_until_departure)
|
|
|
+ return self.state
|
|
|
+
|
|
|
+
|
|
|
+class RoutePlanner(AtomicDEVS):
|
|
|
+ def __init__(self, name):
|
|
|
+ super(RoutePlanner, self).__init__(name)
|
|
|
+
|
|
|
+ self.graph = get_graph()
|
|
|
+
|
|
|
+ self.state = {
|
|
|
+ "request": []
|
|
|
+ }
|
|
|
+
|
|
|
+ self.req_in = self.addInPort("req_in")
|
|
|
+ self.req_out = self.addOutPort("req_out")
|
|
|
+
|
|
|
+ def timeAdvance(self):
|
|
|
+ if len(self.state["request"]) == 0:
|
|
|
+ return INFINITY
|
|
|
+ return 0.0
|
|
|
+
|
|
|
+ def extTransition(self, inputs):
|
|
|
+ if self.req_in in inputs:
|
|
|
+ for request in inputs[self.req_in]:
|
|
|
+ tug = TUGS[TUGS["MMSI"] == str(request["mmsi"])].iloc[0]
|
|
|
+ request["name"] = tug["NAME"]
|
|
|
+
|
|
|
+ request["source_lon"], request["source_lat"] = eval(request["source"])
|
|
|
+ request["target_lon"], request["target_lat"] = eval(request["target"])
|
|
|
+
|
|
|
+ src_vertex, _ = get_closest_vertex(self.graph, request["source_lon"], request["source_lat"])
|
|
|
+ tgt_vertex, _ = get_closest_vertex(self.graph, request["target_lon"], request["target_lat"])
|
|
|
+
|
|
|
+ request["distance"] = self.graph.distances([src_vertex.index], [tgt_vertex.index], weights="distance", mode="all")[0][0]
|
|
|
+
|
|
|
+ # TODO: Use the fastest possible ecological velocity to apply to the trajectory
|
|
|
+ # Doing the task faster than ETA-start is also allowed!
|
|
|
+ request["velocity"] = request["distance"] / (request["ETA"] - request["start"])
|
|
|
+ request["velocity"] = max(request["velocity"], Vessel.ecological()[1])
|
|
|
+
|
|
|
+ self.state["request"].append(request)
|
|
|
+ return self.state
|
|
|
+
|
|
|
+ def outputFnc(self):
|
|
|
+ if len(self.state["request"]) == 0:
|
|
|
+ return {}
|
|
|
+ return { self.req_out: [self.state["request"][0]] }
|
|
|
+
|
|
|
+ def intTransition(self):
|
|
|
+ if len(self.state["request"]) > 0:
|
|
|
+ self.state["request"].pop(0)
|
|
|
+ return self.state
|
|
|
+
|
|
|
+
|
|
|
+class Clock(AtomicDEVS):
|
|
|
+ def __init__(self, name, interval=1):
|
|
|
+ super(Clock, self).__init__(name)
|
|
|
+ self.interval = interval
|
|
|
+ self.state = {
|
|
|
+ "time": 0.0
|
|
|
+ }
|
|
|
+ self.outp = self.addOutPort("outp")
|
|
|
+
|
|
|
+ def timeAdvance(self):
|
|
|
+ return self.interval
|
|
|
+
|
|
|
+ def outputFnc(self):
|
|
|
+ return {
|
|
|
+ self.outp: [True]
|
|
|
+ }
|
|
|
+
|
|
|
+ def intTransition(self):
|
|
|
+ self.state["time"] += self.timeAdvance()
|
|
|
+ return self.state
|