|
@@ -0,0 +1,153 @@
|
|
|
|
+#!/usr/bin/env python
|
|
|
|
+
|
|
|
|
+import time
|
|
|
|
+
|
|
|
|
+from datetime import datetime as dt
|
|
|
|
+from datetime import timedelta
|
|
|
|
+
|
|
|
|
+import pytz
|
|
|
|
+
|
|
|
|
+from aman.config.RHC import RHC
|
|
|
|
+from aman.sys.RecedingHorizonWindow import RecedingHorizonWindow
|
|
|
|
+from aman.types.Inbound import Inbound
|
|
|
|
+
|
|
|
|
+class RecedingHorizonControl:
|
|
|
|
+ def __init__(self, config : RHC):
|
|
|
|
+ self.Windows = []
|
|
|
|
+ # contains the current index and the missed update counter
|
|
|
|
+ self.AssignedWindow = {}
|
|
|
|
+ self.Configuration = config
|
|
|
|
+ self.FreezedIndex = int(self.Configuration.FixedBeforeArrival.seconds / self.Configuration.WindowSize)
|
|
|
|
+
|
|
|
|
+ def update(self, inbound : Inbound):
|
|
|
|
+ # check if we need to update
|
|
|
|
+ if inbound.Report.aircraft.callsign in self.AssignedWindow:
|
|
|
|
+ index = self.AssignedWindow[inbound.Report.aircraft.callsign][0]
|
|
|
|
+ self.AssignedWindow[inbound.Report.aircraft.callsign][1] = 0
|
|
|
|
+
|
|
|
|
+ # check if we assume the scheduling as fixed
|
|
|
|
+ if index < self.FreezedIndex:
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ plannedInbound = self.Windows[index].inbound(inbound.Report.aircraft.callsign)
|
|
|
|
+ plannedInbound.CurrentPosition = inbound.CurrentPosition
|
|
|
|
+
|
|
|
|
+ # check if not accessed by the optimizer
|
|
|
|
+ if plannedInbound.InitialArrivalTime == plannedInbound.EstimatedArrivalTime:
|
|
|
|
+ notPlanned = True
|
|
|
|
+ update = True
|
|
|
|
+ # check if the ITA is greater or the ETA
|
|
|
|
+ elif plannedInbound.EstimatedArrivalTime != inbound.InitialArrivalTime:
|
|
|
|
+ notPlanned = False
|
|
|
|
+ update = True
|
|
|
|
+
|
|
|
|
+ # update the windows
|
|
|
|
+ if True == update:
|
|
|
|
+ if inbound.InitialArrivalTime >= self.Windows[index].StartTime and inbound.InitialArrivalTime < self.Windows[index].EndTime:
|
|
|
|
+ keepInWindow = True
|
|
|
|
+ else:
|
|
|
|
+ keepInWindow = False
|
|
|
|
+
|
|
|
|
+ # check if planning can be updated or needs to be due to a delay
|
|
|
|
+ if True == notPlanned or plannedInbound.Inbound.EstimatedArrivalTime < inbound.InitialArrivalTime:
|
|
|
|
+ # keep it in this bucket
|
|
|
|
+ if True == keepInWindow:
|
|
|
|
+ plannedInbound.EstimatedArrivalTime = inbound.InitialArrivalTime
|
|
|
|
+ plannedInbound.InitialArrivalTime = inbound.InitialArrivalTime
|
|
|
|
+
|
|
|
|
+ self.Windows[index].Inbounds.sort(key = lambda x: x.EstimatedArrivalTime)
|
|
|
|
+ # put it in the new bucket
|
|
|
|
+ else:
|
|
|
|
+ self.Windows[index].remove(inbound.Report.aircraft.callsign)
|
|
|
|
+ self.AssignedWindow.pop(inbound.Report.aircraft.callsign)
|
|
|
|
+ self.update(inbound)
|
|
|
|
+ else:
|
|
|
|
+ inserted = False
|
|
|
|
+ for i in range(0, len(self.Windows)):
|
|
|
|
+ window = self.Windows[i]
|
|
|
|
+
|
|
|
|
+ # find the correct window
|
|
|
|
+ if window.StartTime <= inbound.EstimatedArrivalTime and window.EndTime > inbound.EstimatedArrivalTime:
|
|
|
|
+ if i > self.FreezedIndex:
|
|
|
|
+ self.AssignedWindow[inbound.Report.aircraft.callsign] = [ i, 0 ]
|
|
|
|
+ window.insert(inbound)
|
|
|
|
+ inserted = True
|
|
|
|
+ break
|
|
|
|
+
|
|
|
|
+ # create the new window
|
|
|
|
+ if False == inserted:
|
|
|
|
+ if 0 != len(self.Windows):
|
|
|
|
+ lastWindowTime = self.Windows[-1].EndTime
|
|
|
|
+ else:
|
|
|
|
+ lastWindowTime = dt.utcfromtimestamp(int(time.time())).replace(tzinfo = pytz.UTC)
|
|
|
|
+ timestep = timedelta(seconds = self.Configuration.WindowSize)
|
|
|
|
+
|
|
|
|
+ while True:
|
|
|
|
+ self.Windows.append(RecedingHorizonWindow(lastWindowTime, lastWindowTime + timestep))
|
|
|
|
+ if self.Windows[-1].EndTime > inbound.EstimatedArrivalTime:
|
|
|
|
+ self.AssignedWindow[inbound.Report.aircraft.callsign] = [ len(self.Windows) - 1, 0 ]
|
|
|
|
+ self.Windows[-1].insert(inbound)
|
|
|
|
+ self.Windows[-1].Inbounds.sort(key = lambda x: x.EstimatedArrivalTime)
|
|
|
|
+ break
|
|
|
|
+ lastWindowTime = self.Windows[-1].EndTime
|
|
|
|
+
|
|
|
|
+ def lastFixedInboundOnRunway(self, runway : str):
|
|
|
|
+ # no inbounds available
|
|
|
|
+ if 0 == len(self.Windows):
|
|
|
|
+ return None
|
|
|
|
+
|
|
|
|
+ # search from the back to the front to find the last inbound
|
|
|
|
+ for i in range(min(self.FreezedIndex, len(self.Windows)), -1, -1):
|
|
|
|
+ for inbound in self.Windows[i].Inbounds:
|
|
|
|
+ if runway == inbound.PlannedRunway.Runway.name:
|
|
|
|
+ return inbound
|
|
|
|
+
|
|
|
|
+ # no inbound found
|
|
|
|
+ return None
|
|
|
|
+
|
|
|
|
+ def optimizationRelevantInbounds(self):
|
|
|
|
+ # no new inbounds
|
|
|
|
+ if len(self.Windows) <= self.FreezedIndex:
|
|
|
|
+ return None
|
|
|
|
+
|
|
|
|
+ inbounds = []
|
|
|
|
+
|
|
|
|
+ # check the overlapping windows
|
|
|
|
+ for i in range(self.FreezedIndex + 1, min(len(self.Windows), self.FreezedIndex + 1 + self.Configuration.WindowOverlap)):
|
|
|
|
+ for inbound in self.Windows[i].Inbounds:
|
|
|
|
+ inbounds.append(inbound)
|
|
|
|
+
|
|
|
|
+ # check if we found relevant inbounds
|
|
|
|
+ if 0 != len(inbounds):
|
|
|
|
+ return inbounds
|
|
|
|
+ else:
|
|
|
|
+ return None
|
|
|
|
+
|
|
|
|
+ def cleanupWindows(self):
|
|
|
|
+ currentUtc = dt.utcfromtimestamp(int(time.time())).replace(tzinfo = pytz.UTC)
|
|
|
|
+ offsetCorrection = 0
|
|
|
|
+
|
|
|
|
+ # delete the non-required windows
|
|
|
|
+ while 0 != len(self.Windows) and currentUtc > self.Windows[0].EndTime:
|
|
|
|
+ # cleanup the association table
|
|
|
|
+ for inbound in self.Windows[0].Inbounds:
|
|
|
|
+ self.AssignedWindow.pop(inbound.Report.aircraft.callsign)
|
|
|
|
+
|
|
|
|
+ offsetCorrection += 1
|
|
|
|
+ self.Windows.pop(0)
|
|
|
|
+
|
|
|
|
+ # correct the association table
|
|
|
|
+ if 0 != offsetCorrection:
|
|
|
|
+ for callsign in self.AssignedWindow:
|
|
|
|
+ self.AssignedWindow[callsign][0] -= offsetCorrection
|
|
|
|
+
|
|
|
|
+ # delete the non-updated aircrafts and increase the missed-counter for later runs
|
|
|
|
+ callsigns = []
|
|
|
|
+ for callsign in self.AssignedWindow:
|
|
|
|
+ if 2 < self.AssignedWindow[callsign][1]:
|
|
|
|
+ self.Windows[self.AssignedWindow[callsign][0]].remove(callsign)
|
|
|
|
+ callsigns.append(callsign)
|
|
|
|
+ self.AssignedWindow[callsign][1] += 1
|
|
|
|
+
|
|
|
|
+ for callsign in callsigns:
|
|
|
|
+ self.AssignedWindow.pop(callsign)
|