#!/usr/bin/env python import copy import time from datetime import datetime as dt from datetime import timedelta import pytz from aman.config.Airport import Airport from aman.config.AirportSequencing import AirportSequencing from aman.config.RHC import RHC from aman.sys.aco.Node import Node 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 insertInWindow(self, inbound : Inbound, usePTA : bool): if False == usePTA: referenceTime = inbound.EnrouteArrivalTime else: referenceTime = inbound.PlannedArrivalTime inserted = False for i in range(0, len(self.Windows)): window = self.Windows[i] # find the correct window if window.StartTime <= referenceTime and window.EndTime > referenceTime: self.AssignedWindow[inbound.Callsign] = [ i, 0 ] inbound.FixedSequence = i < self.FreezedIndex if True == inbound.FixedSequence and None == inbound.PlannedArrivalTime: inbound.PlannedArrivalTime = inbound.EnrouteArrivalTime 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 > referenceTime: window = self.Windows[-1] window.insert(inbound) self.AssignedWindow[inbound.Callsign] = [ len(self.Windows) - 1, 0 ] inbound.FixedSequence = len(self.Windows) < self.FreezedIndex if True == inbound.FixedSequence and None == inbound.PlannedArrivalTime: inbound.PlannedArrivalTime = inbound.EnrouteArrivalTime break lastWindowTime = self.Windows[-1].EndTime window.Inbounds.sort(key = lambda x: x.PlannedArrivalTime if None != x.PlannedArrivalTime else x.EnrouteArrivalTime) def updateReport(self, inbound : Inbound): # check if we need to update if inbound.Callsign in self.AssignedWindow: index = self.AssignedWindow[inbound.Callsign][0] self.AssignedWindow[inbound.Callsign][1] = 0 plannedInbound = self.Windows[index].inbound(inbound.Callsign) plannedInbound.Report = inbound.Report plannedInbound.ReportTime = inbound.ReportTime plannedInbound.CurrentPosition = inbound.CurrentPosition plannedInbound.RequestedRunway = inbound.RequestedRunway # ingore fixed updates if True == plannedInbound.FixedSequence or index <= self.FreezedIndex: plannedInbound.FixedSequence = True return plannedInbound.WTC = inbound.WTC # check if we need to update the inbound if None == plannedInbound.PlannedStar: reference = inbound.EnrouteArrivalTime if plannedInbound.EnrouteArrivalTime > reference: reference = plannedInbound.EnrouteArrivalTime if reference < self.Windows[index].StartTime or reference >= self.Windows[index].EndTime: self.Windows[index].remove(inbound.Callsign) self.AssignedWindow.pop(inbound.Callsign) inbound.EnrouteArrivalTime = reference self.updateReport(inbound) else: plannedInbound.EnrouteArrivalTime = reference self.Windows[index].Inbounds.sort(key = lambda x: x.PlannedArrivalTime if None != x.PlannedArrivalTime else x.EnrouteArrivalTime) else: self.insertInWindow(inbound, False) def resequenceInbound(self, inbound : Inbound): index = self.AssignedWindow[inbound.Callsign][0] sequenced = self.Windows[index].inbound(inbound.Callsign) if None == sequenced: return # resynchronized the planned information sequenced.PlannedRunway = inbound.PlannedRunway sequenced.PlannedStar = inbound.PlannedStar sequenced.PlannedArrivalRoute = inbound.PlannedArrivalRoute sequenced.PlannedArrivalTime = inbound.PlannedArrivalTime sequenced.InitialArrivalTime = inbound.InitialArrivalTime sequenced.PlannedTrackmiles = inbound.PlannedTrackmiles sequenced.AssignmentMode = inbound.AssignmentMode sequenced.ExpectedRunway = inbound.ExpectedRunway sequenced.HasValidSequence = True # resort the inbound if sequenced.PlannedArrivalTime < self.Windows[index].StartTime or sequenced.PlannedArrivalTime >= self.Windows[index].EndTime: self.Windows[index].remove(sequenced.Callsign) self.AssignedWindow.pop(sequenced.Callsign) self.insertInWindow(sequenced, True) else: sequenced.FixedSequence = index < self.FreezedIndex self.Windows[index].Inbounds.sort(key = lambda x: x.PlannedArrivalTime if None != x.PlannedArrivalTime else x.EnrouteArrivalTime) def latestFixedInbounds(self, configuration : Airport, sequenceConfiguration : AirportSequencing): if 0 == len(self.Windows): return None, None # create the runway tree runwayInbounds = {} for runway in sequenceConfiguration.ActiveArrivalRunways: runwayInbounds[runway.Runway.Name] = None # create the IAF tree iafInbounds = {} for star in configuration.ArrivalRouteConstraints: altitude = configuration.ArrivalRouteConstraints[star][0].Altitude iaf = configuration.ArrivalRouteConstraints[star][0].Name if iaf not in iafInbounds: iafInbounds[iaf] = { altitude : None } elif altitude not in iafInbounds[iaf]: iafInbounds[iaf][altitude] = None # associate the inbounds to the runways and the IAFs for i in range(min(self.FreezedIndex, len(self.Windows)), -1, -1): for inbound in self.Windows[i].Inbounds: if None == inbound.PlannedRunway or None == inbound.PlannedArrivalRoute: continue node = Node(inbound, None, None, None, None) if inbound.PlannedRunway.Name in runwayInbounds: if None == runwayInbounds[inbound.PlannedRunway.Name] or runwayInbounds[inbound.PlannedRunway.Name].Inbound.PlannedArrivalTime < node.Inbound.PlannedArrivalTime: runwayInbounds[inbound.PlannedRunway.Name] = node if inbound.PlannedArrivalRoute[0].Waypoint.Name in iafInbounds: delta = 100000.0 targetLevel = None for level in iafInbounds[inbound.PlannedArrivalRoute[0].Waypoint.Name]: difference = abs(level - inbound.PlannedArrivalRoute[0].Altitude) if difference < delta: delta = difference targetLevel = level if None == iafInbounds[inbound.PlannedArrivalRoute[0].Waypoint.Name][targetLevel]: iafInbounds[inbound.PlannedArrivalRoute[0].Waypoint.Name][targetLevel] = node elif iafInbounds[inbound.PlannedArrivalRoute[0].Waypoint.Name][targetLevel].Inbound.PlannedArrivalTime < node.Inbound.PlannedArrivalTime: iafInbounds[inbound.PlannedArrivalRoute[0].Waypoint.Name][targetLevel] = node return runwayInbounds, iafInbounds def optimizationRelevantInbounds(self): inbounds = [] earliestArrivalTime = self.Windows[self.FreezedIndex + 1].StartTime # check if we have a reconnect in the freezed blocks (VATSIM specific behavior) for i in range(0, self.FreezedIndex + 1): for inbound in self.Windows[i].Inbounds: if False == inbound.HasValidSequence: inbounds.sort(key = lambda x: x.PlannedArrivalTime if None != x.PlannedArrivalTime else x.EnrouteArrivalTime) inbounds.append(copy.deepcopy(inbound)) # no new inbounds if len(self.Windows) <= self.FreezedIndex + 1: if 0 == len(inbounds): return None, None else: return inbounds, earliestArrivalTime # check the overlapping windows for i in range(self.FreezedIndex + 1, len(self.Windows)): for inbound in self.Windows[i].Inbounds: inbounds.append(copy.deepcopy(inbound)) if 20 <= len(inbounds): break # check if we found relevant inbounds if 0 != len(inbounds): inbounds.sort(key = lambda x: x.PlannedArrivalTime if None != x.PlannedArrivalTime else x.EnrouteArrivalTime) return inbounds, earliestArrivalTime else: return None, None def sequence(self): inbounds = [] for i in range(0, len(self.Windows)): for inbound in self.Windows[i].Inbounds: if True == inbound.HasValidSequence: inbounds.append(inbound) return inbounds 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.Callsign) offsetCorrection += 1 self.Windows.pop(0) # correct the association table if 0 != offsetCorrection: for callsign in self.AssignedWindow: self.AssignedWindow[callsign][0] -= offsetCorrection if self.AssignedWindow[callsign][0] <= self.FreezedIndex: self.Windows[self.AssignedWindow[callsign][0]].inbound(callsign).FixedSequence = True # 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)