diff --git a/aman/sys/RecedingHorizonControl.py b/aman/sys/RecedingHorizonControl.py new file mode 100644 index 0000000..b289847 --- /dev/null +++ b/aman/sys/RecedingHorizonControl.py @@ -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) diff --git a/aman/sys/RecedingHorizonWindow.py b/aman/sys/RecedingHorizonWindow.py new file mode 100644 index 0000000..93f6741 --- /dev/null +++ b/aman/sys/RecedingHorizonWindow.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python + +from aman.types.Inbound import Inbound + +class RecedingHorizonWindow: + def __init__(self, startTime, endTime): + self.StartTime = startTime + self.EndTime = endTime + self.Inbounds = [] + + def isInWindow(self, inbound : Inbound): + for report in self.Inbounds: + if report.Report.aircraft.callsign == inbound.Report.aircraft.callsign: + return True + return False + + def inbound(self, callsign : str): + for report in self.Inbounds: + if report.Report.aircraft.callsign == callsign: + return report + return None + + def insert(self, inbound : Inbound): + for i in range(0, len(self.Inbounds)): + if self.Inbounds[i].Report.aircraft.callsign == inbound.Report.aircraft.callsign: + return + self.Inbounds.append(inbound) + + def remove(self, callsign : str): + for i in range(0, len(self.Inbounds)): + if self.Inbounds[i].Report.aircraft.callsign == callsign: + self.Inbounds.pop(i) + return \ No newline at end of file