Merge branch 'feature/setup' into 'develop'

Feature/setup

See merge request nav/aman/aman-sys!1
This commit is contained in:
Sven Czarnian
2021-09-03 21:41:00 +00:00
28 changed files with 957 additions and 3 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
Backend.ini
main.py
.vscode/
*_pb2.py
__pycache__
*.egg-info
build/
dist/

4
.gitmodules vendored Normal file
View File

@@ -0,0 +1,4 @@
[submodule "src/protobuf"]
path = src/protobuf
url = git@git.vatsim-germany.org:nav/aman-com.git
branch = feature/protobuf

View File

@@ -1,7 +1,23 @@
# aman-sys # Arrival MANanager (AMAN)
Dieses Repository stellt den VATGER AMAN Server bereit, dessen Aufgabe es ist, einen optimalen Arrivalflow für die Flugplätze in der VACC zu errechnen. ## System description
AMAN is splitted up into four different components.
* aman-com defines the diffent message types
* aman-es implements an EuroScope plugin to communicate with [aman-sys](https://git.vatsim-germany.org/nav/aman-sys)
* aman-sys implements the backend system to plan an optimal arrival sequence for the different airports
* aman-web implements a web-interface to configure [aman-sys](https://git.vatsim-germany.org/nav/aman-sys) and visualize sequences
## Component description
AMAN uses [Protocol Buffers](https://developers.google.com/protocol-buffers)
for the message serialization and message definition between the EuroScope instance and the AMAN backend.
Additionally is [ZeroMQ](https://zeromq.org/) used for the communication abstraction layer.
This component provides the server backend with the planning and optimization system per airport.
It is designed as a python framework that can run on a webserver.
ZMQ based encryption and authentication methods are used to authenticate controllers.
## RHC-ACS-ASS Algorithm ## RHC-ACS-ASS Algorithm
Step 1: Initialization. Set up parameters for Step 1: Initialization. Set up parameters for
@@ -41,4 +57,13 @@ Step 3.3: Calculate the fitness of each ant and determine
the best solution. Moreover, the current best solution is compared with the historically best solution the best solution. Moreover, the current best solution is compared with the historically best solution
to determine the historically best solution. to determine the historically best solution.
Step 3.4: Perform the global pheromone updating as (10). Step 3.4: Perform the global pheromone updating as (10).
# Additional libraries
* [ZeroMQ](https://github.com/zeromq) - GNU GPLv3
* [Protocol Buffers](https://github.com/protocolbuffers/protobuf) - BSD-3
# License
AMAN is released under the [GNU General Public License v3](LICENSE)

82
aman/AMAN.py Normal file
View File

@@ -0,0 +1,82 @@
#!/usr/bin/env python
import glob
import os
import sys
from aman.com import AircraftReport_pb2
from aman.com.Euroscope import Euroscope
from aman.config.AircraftPerformance import AircraftPerformance
from aman.config.Airport import Airport
from aman.config.System import System
from aman.sys.Worker import Worker
class AMAN:
def findConfigPath():
envvar = os.environ.get('AMAN_CONFIG_PATH')
if None == envvar:
print('No AMAN_CONFIG_PATH in environment variables found. Using execution directory.')
path = os.getcwd()
else:
print('AMAN_CONFIG_PATH found.')
path = envvar
print('Config-path: ' + path)
return path
def __init__(self):
# default initialization of members
self.systemConfig = None
self.aircraftPerformance = None
self.receiver = None
self.workers = []
self.inbounds = {}
configPath = AMAN.findConfigPath()
# read all system relevant configuration files
self.systemConfig = System(os.path.join(configPath, 'System.ini'))
print('Parsed System.ini')
# read the aircraft performance data
self.aircraftPerformance = AircraftPerformance(os.path.join(configPath, 'PerformanceData.ini'))
if None == self.aircraftPerformance:
sys.stderr.write('No aircraft performance data found!')
sys.exit(-1)
else:
print('Parsed PerformanceData.ini. Extracted ' + str(len(self.aircraftPerformance.aircrafts)) + ' aircrafts')
# find the airport configurations and create the workers
airportsPath = os.path.join(os.path.join(configPath, 'airports'), '*.ini')
for file in glob.glob(airportsPath):
icao = os.path.splitext(os.path.basename(file))[0]
print('Parsing planner configuration for ' + icao)
airportConfig = Airport(file, icao)
# initialize the worker thread
worker = Worker(icao, airportConfig)
worker.start()
self.workers.append(worker)
print('Starter worker for ' + icao)
# create the EuroScope receiver
self.receiver = Euroscope(configPath, self.systemConfig.Server, self)
def __del__(self):
if None != self.receiver:
del self.receiver
self.receiver = None
for worker in self.workers:
worker.stop()
def updateAircraftReport(self, report : AircraftReport_pb2.AircraftReport):
# find the correct worker for the inbound
for worker in self.workers:
if worker.icao == report.destination:
print('Updated ' + report.aircraft.callsign + ' for ' + worker.icao)
worker.acquire()
worker.reportQueue[report.aircraft.callsign] = report
worker.release()
break

2
aman/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
import com
import tools

104
aman/com/Euroscope.py Normal file
View File

@@ -0,0 +1,104 @@
#!/usr/bin/env python
import ctypes
import glob
import os
import sys
import threading
import time
import zmq
import zmq.auth
from aman.com import AircraftReport_pb2
from aman.config.Server import Server
class ReceiverThread(threading.Thread):
def __init__(self, socket, aman):
threading.Thread.__init__(self)
self.socket = socket
self.aman = aman
def run(self):
while True:
try:
msg = self.socket.recv(zmq.NOBLOCK)
# parse the received message
report = AircraftReport_pb2.AircraftReport()
report.ParseFromString(msg)
# try to associate the received aircraft to an airport
self.aman.updateAircraftReport(report)
except zmq.ZMQError as error:
if zmq.EAGAIN == error.errno:
time.sleep(0.5)
continue
else:
return
def threadId(self):
if hasattr(self, '_thread_id'):
return self._thread_id
for id, thread in threading._active.items():
if thread is self:
return id
def stopThread(self):
id = self.threadId()
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(id, ctypes.py_object(SystemExit))
if 1 < res:
ctypes.pythonapi.PyThreadState_SetAsyncExc(id, 0)
# @brief Receives and sends messages to EuroScope plugins
class Euroscope:
# @brief Initializes the ZMQ socket
# @param[in] config The server configuration
def __init__(self, configPath : str, config : Server, aman):
self.context = zmq.Context()
# find the key directories
serverKeyPath = os.path.join(os.path.join(configPath, 'keys'), 'server')
if False == os.path.isdir(serverKeyPath):
sys.stderr.write('No directory for the server key found')
sys.exit(-1)
print('Path to the server key: ' + serverKeyPath)
clientKeyPath = os.path.join(os.path.join(configPath, 'keys'), 'clients')
if False == os.path.isdir(clientKeyPath):
sys.stderr.write('No directory for the client keys found')
sys.exit(-1)
print('Path to the client keys: ' + clientKeyPath)
# read the certificates
keyPairPath = glob.glob(os.path.join(serverKeyPath, '*.key_secret'))
if 1 != len(keyPairPath):
sys.stderr.write('No public-private keypair found for the server certificate')
sys.exit(-1)
keyPair = zmq.auth.load_certificate(keyPairPath[0])
# initialize the receiver
self.receiverSocket = zmq.Socket(self.context, zmq.SUB)
self.receiverSocket.setsockopt(zmq.CURVE_PUBLICKEY, keyPair[0])
self.receiverSocket.setsockopt(zmq.CURVE_SECRETKEY, keyPair[1])
self.receiverSocket.setsockopt(zmq.CURVE_SERVER, True)
self.receiverSocket.bind('tcp://' + config.Address + ':' + str(config.PortReceiver))
self.receiverSocket.setsockopt(zmq.SUBSCRIBE, b'')
self.receiverThread = ReceiverThread(self.receiverSocket, aman)
self.receiverThread.start()
print('Listening at tcp://' + config.Address + ':' + str(config.PortReceiver))
# initialize the notification
self.notificationSocket = zmq.Socket(self.context, zmq.PUB)
self.notificationSocket.setsockopt(zmq.CURVE_PUBLICKEY, keyPair[0])
self.notificationSocket.setsockopt(zmq.CURVE_SECRETKEY, keyPair[1])
self.notificationSocket.setsockopt(zmq.CURVE_SERVER, True)
self.notificationSocket.bind('tcp://' + config.Address + ':' + str(config.PortNotification))
print('Publishing at tcp://' + config.Address + ':' + str(config.PortNotification))
def __del__(self):
self.receiverThread.stopThread()
self.receiverThread.join()
self.receiverSocket.close()
self.notificationSocket.close()

0
aman/com/__init__.py Normal file
View File

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env python
import configparser
from aman.types.PerformanceData import PerformanceData
class AircraftPerformance:
def __init__(self, filepath : str):
config = configparser.ConfigParser()
config.read(filepath)
self.aircrafts = { }
# iterate over all entries
for key in config:
if 'DEFAULT' == key:
continue
aircraft = PerformanceData(key)
aircraft.speedAboveFL240 = config[key]['speedabovefl240']
aircraft.rodAboveFL240 = config[key]['rodabovefl240']
aircraft.speedAboveFL100 = config[key]['speedabovefl100']
aircraft.rodAboveFL100 = config[key]['rodabovefl100']
aircraft.speedBelowFL100 = config[key]['speedbelowfl100']
aircraft.rodBelowFL100 = config[key]['rodbelowfl100']
aircraft.speedApproach = config[key]['speedapproach']
self.aircrafts[aircraft.icao] = aircraft

66
aman/config/Airport.py Normal file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env python
import configparser
import glob
import os
import sys
from formats.SctEseFormat import SctEseFormat
class Airport:
def findGngData(data, path):
if None == data.get('gngwildcard'):
return None, None
# find the newest ESE file
files = glob.glob(os.path.join(path, data['gngwildcard'] + '.ese'))
latestEse = max(files, key=os.path.getctime)
# search for the corresponding SCT file
latestSct = os.path.splitext(latestEse)[0] + '.sct'
# check if the files exist
if False == os.path.isfile(latestEse) or False == os.path.isfile(latestSct):
return None, None
return latestSct, latestEse
def parsePlanning(self, planning):
if None == planning.get('routes'):
return []
return planning['routes'].split(':')
def __init__(self, filepath : str, icao : str):
self.arrivalRoutes = {}
config = configparser.ConfigParser()
config.read(filepath)
dataConfig = None
planningConfig = None
# search the required sections
for key in config:
if 'DATA' == key:
dataConfig = config['DATA']
elif 'PLANNING' == key:
planningConfig = config['PLANNING']
# find the GNG-file data
sctFile, eseFile = Airport.findGngData(dataConfig, os.path.dirname(filepath))
if None == sctFile or None == eseFile:
sys.stderr.write('No GNG-files found')
sys.exit(-1)
# parse the planning information
if None == planningConfig or False == self.parsePlanning(planningConfig):
sys.stderr.write('No planning configuration found')
sys.exit(-1)
requiredArrivalRoutes = self.parsePlanning(planningConfig)
if 0 == len(requiredArrivalRoutes):
sys.stderr.write('No valid planning configuration found')
sys.exit(-1)
# parse the GNG data
print('Used GNG-Data: ' + eseFile)
self.gngData = SctEseFormat(sctFile, eseFile, icao, requiredArrivalRoutes)

29
aman/config/Server.py Normal file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env python
import configparser;
import sys
class Server():
def __init__(self, config : configparser.ConfigParser):
self.Address = None
self.PortReceiver = None
self.PortNotification = None
# search the required sections
for key in config:
if 'address' == key:
self.Address = config['address']
elif 'portreceiver' == key:
self.PortReceiver = int(config['portreceiver'])
elif 'portnotification' == key:
self.PortNotification = int(config['portnotification'])
if self.Address is None:
sys.stderr.write('No server-address configuration found!')
sys.exit(-1)
if self.PortReceiver is None:
sys.stderr.write('No server-port-receiver configuration found!')
sys.exit(-1)
if self.PortNotification is None:
sys.stderr.write('No server-port-notification configuration found!')
sys.exit(-1)

23
aman/config/System.py Normal file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env python
import configparser
import sys
from aman.config.Server import Server
class System:
def __init__(self, filepath : str):
config = configparser.ConfigParser()
config.read(filepath)
# search the required sections
serverSectionAvailable = False
for key in config:
if 'SERVER' == key:
serverSectionAvailable = True
if not serverSectionAvailable:
sys.stderr.write('No server-configuration section found!')
sys.exit(-1)
self.Server = Server(config['SERVER'])

0
aman/config/__init__.py Normal file
View File

View File

@@ -0,0 +1,152 @@
#!/usr/bin/env python
import sys
from aman.types.ArrivalRoute import ArrivalRoute
from aman.types.Waypoint import Waypoint
class SctEseFormat:
def readFile(filename : str):
fileBlocks = {}
block = None
# read the file line by line and create segments based on []-entries
with open(filename) as file:
for line in file:
line = line.strip()
# found a new segment
if line.startswith('['):
block = line[1:-1]
fileBlocks.setdefault(block, [])
# append the last backend
elif None != block and 0 != len(line):
fileBlocks[block].append(line)
return fileBlocks
def parseWaypoint(waypoint : str, nameIdx : int, latitudeIdx : int, longitudeIdx : int):
split = list(filter(None, waypoint.split(' ')))
if len(split) <= longitudeIdx:
sys.stderr.write('Invalid waypoint format: ' + waypoint)
sys.exit(-1)
return Waypoint(split[nameIdx], Waypoint.dms2dd(split[latitudeIdx]), Waypoint.dms2dd(split[longitudeIdx]))
def extractWaypoints(self, sctFilepath : str):
config = SctEseFormat.readFile(sctFilepath)
foundAirports = False
foundVOR = False
foundNDB = False
foundFix = False
for key in config:
if 'VOR' == key:
foundVOR = True
elif 'NDB' == key:
foundNDB = True
elif 'FIXES' == key:
foundFix = True
elif 'AIRPORT' == key:
foundAirports = True
if False == foundVOR:
sys.stderr.write('Unable to find VOR-entries in the sector file')
sys.exit(-1)
if False == foundNDB:
sys.stderr.write('Unable to find NDB-entries in the sector file')
sys.exit(-1)
if False == foundFix:
sys.stderr.write('Unable to find FIX-entries in the sector file')
sys.exit(-1)
if False == foundAirports:
sys.stderr.write('Unable to find AIRPORT-entries in the sector file')
sys.exit(-1)
# extract all waypoints
for waypoint in config['VOR']:
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 2, 3)
self.waypoints.setdefault(waypoint.name, []).append(waypoint)
for waypoint in config['NDB']:
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 1, 2)
self.waypoints.setdefault(waypoint.name, []).append(waypoint)
for waypoint in config['FIXES']:
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 1, 2)
self.waypoints.setdefault(waypoint.name, []).append(waypoint)
# extract the airports
for airport in config['AIRPORT']:
airport = SctEseFormat.parseWaypoint(airport,0, 2, 3)
self.airports.setdefault(airport.name, []).append(airport)
def parseArrivalRoute(self, route : str, airport : Waypoint):
# split the route and validate that it is a STAR for the airport
split = route.split(':')
if 5 != len(split) or 'STAR' != split[0] or split[1] != airport.name:
return None
# find all waypoints
waypoints = []
route = list(filter(None, split[4].split(' ')))
for waypoint in route:
# find the waypoint in the route
coordinates = self.waypoints[waypoint]
# no waypoint with this name defined
if None == coordinates:
sys.stderr.write('Unable to find waypoint ' + waypoint)
sys.exit(-1)
# multiple waypoints, but use Haversine distance to distinct between candidates
elif 1 != len(coordinates):
minDistance = sys.float_info.max
nearest = None
# we assume that waypoints with the same name are not close each other
for coordinate in coordinates:
distance = coordinate.haversine(airport)
# found a closer waypoint
if minDistance > distance:
minDistance = distance
nearest = coordinate
if None == nearest:
sys.stderr.write('Unable to find a close waypoint for ' + waypoint)
sys.exit(-1)
waypoints.append(nearest)
# extend the list of waypoints
else:
waypoints.append(coordinates[0])
# create the arrival route
return ArrivalRoute(split[3], split[2], waypoints)
def extractArrivalRoutes(self, eseFilepath : str, airport : str, allowedRoutes : list):
config = SctEseFormat.readFile(eseFilepath)
foundSidsStars = False
# search the airport in the extracted list
if not airport in self.airports:
sys.stderr.write(airport + 'in self.airports', 'Unable to find the requested airport')
sys.exit(-1)
airport = self.airports[airport][0]
for key in config:
if 'SIDSSTARS' == key:
foundSidsStars = True
if False == foundSidsStars:
sys.stderr.write('Unable to find SIDSSTARS-entries in the sector file')
sys.exit(-1)
# parse all arrival routes
for line in config['SIDSSTARS']:
route = self.parseArrivalRoute(line, airport)
if None != route and route.name in allowedRoutes:
self.arrivalRoutes.setdefault(route.runway, []).append(route)
def __init__(self, sctFilepath : str, eseFilepath : str, airport : str, allowedRoutes : list):
self.arrivalRoutes = {}
self.waypoints = {}
self.airports = {}
self.extractWaypoints(sctFilepath)
self.extractArrivalRoutes(eseFilepath, airport, allowedRoutes)

0
aman/formats/__init__.py Normal file
View File

33
aman/sys/Worker.py Normal file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env python
from threading import Thread, Lock
import time
from aman.config.Airport import Airport
class Worker(Thread):
def __init__(self, icao : str, configuration : Airport):
Thread.__init__(self)
self.stopThread = None
self.icao = icao
self.configuration = configuration
self.arrivalRoutes = configuration.gngData.arrivalRoutes
self.updateLock = Lock()
self.reportQueue = {}
def stop(self):
self.stopThread = True
def run(self):
counter = 0
while None == self.stopThread:
time.sleep(1)
counter += 1
if 0 != (counter % 60):
continue
# TODO handle the report queue and update internal information
# TODO execute planning, etc.
continue

0
aman/sys/__init__.py Normal file
View File

View File

@@ -0,0 +1,44 @@
#!/usr/bin/env python
import argparse
import os
from typing import Tuple
import zmq.auth
# @brief Creates a new keypair for ZMQ encryption
# @param[in] directory The location where to store the keys
# @return The public and private key tuple
def KeyPairCreator(directory: str, server: bool) -> Tuple[str, str]:
if not server:
print('Creating a new pair for a client...')
target = 'client'
else:
print('Creating a new pair for the server...')
target = 'server'
public, private = zmq.auth.create_certificates(directory, target)
return (public, private)
def str2bool(value):
if isinstance(value, bool):
return value
elif value.lower() in ('yes', 'true', 't', 'y', '1'):
return True
elif value.lower() in ('no', 'false', 'f', 'n', '0'):
return False
else:
raise argparse.ArgumentTypeError('Boolean value expected')
if __name__ == '__main__':
# create the commandline parser
parser = argparse.ArgumentParser(description='Create a new key-value pair')
parser.add_argument('directory', help='Directory where to store the key pair')
parser.add_argument('--server', default=False, action='store_true', help="Creates server key pair")
args = parser.parse_args()
# create the directory if it does not exist
if not os.path.exists(args.directory):
os.makedirs(args.directory)
# create the keys
KeyPairCreator(args.directory, args.server)

View File

@@ -0,0 +1,151 @@
#!/usr/bin/env python
import argparse
import configparser
import os
import urllib.request
from bs4 import BeautifulSoup
from aman.types.PerformanceData import PerformanceData
def findAircraftPages(rooturl : str, suburl : str):
aircrafts = []
with urllib.request.urlopen(rooturl + suburl) as site:
data = site.read().decode('utf-8')
site.close()
parsed = BeautifulSoup(data, features='lxml')
for link in parsed.body.find_all('a', title=True):
split = link['href'].split('/')
if 3 == len(split) and split[2] == link['title'] and 'Category' not in link['title'] and 'Special' not in link['href']:
aircrafts.append(rooturl + link['href'])
for link in parsed.body.find_all('a', attrs={ 'title': 'Category:Aircraft' }):
if 'previous' not in link.text:
aircrafts.extend(findAircraftPages(rooturl, link['href']))
return aircrafts
def findAndParseEntry(tableRow, startIdx, substring, default):
while 0 < startIdx:
if substring in tableRow[startIdx].text:
split = tableRow[startIdx].text.split(' ')
if 1 >= len(split):
return default, startIdx - 2
else:
return int(split[0]), startIdx - 2
else:
startIdx -= 1
return 0, -1
def findAndParseSpeedEntry(tableRow, startIdx, default):
return findAndParseEntry(tableRow, startIdx, 'kts', default)
def findAndParseRodEntry(tableRow, startIdx, default):
return findAndParseEntry(tableRow, startIdx, 'ft/min', default)
def parsePerformanceEntries(tableRowSpeeds, tableRowRODs):
speeds = []
rods = []
# parse the speed data
idx = len(tableRowSpeeds) - 1
while 0 < idx:
parsed = findAndParseSpeedEntry(tableRowSpeeds, idx, 140 if 0 == len(speeds) else 250)
if 0 < idx:
speeds.append(parsed[0])
idx = parsed[1]
# parse the ROD data
idx = len(tableRowRODs) - 1
while 0 < idx:
parsed = findAndParseRodEntry(tableRowRODs, idx, 2000)
if 0 < idx:
rods.append(parsed[0])
idx = parsed[1]
return speeds, rods
def parsePerformanceData(url : str):
with urllib.request.urlopen(url) as site:
data = site.read().decode('utf-8')
site.close()
# check if we find the ICAO code
parsed = BeautifulSoup(data, features='lxml')
icao = parsed.body.find('h5', attrs={ 'id' : 'siteSub', 'class' : 'subtitle'})
if None == icao or '' == icao.text:
return False, None
aircraft = PerformanceData(icao.text)
performanceTable = parsed.body.find('table', attrs={ 'class' : 'wikitable', 'style' : 'font-size: 90%;' })
if None == performanceTable or None == performanceTable.find_all('tr')[1] or None == performanceTable.find_all('tr')[2]:
return False, None
speeds, rods = parsePerformanceEntries(performanceTable.find_all('tr')[1].find_all('td'),
performanceTable.find_all('tr')[2].find_all('td'))
if 10 > len(speeds):
speeds.insert(1, speeds[1])
# create the speed data
if len(speeds) >= 4:
aircraft.speedApproach = speeds[0]
aircraft.speedBelowFL100 = speeds[1]
aircraft.speedAboveFL100 = speeds[2]
aircraft.speedAboveFL240 = speeds[3]
# create the ROD data
if len(rods) >= 3:
aircraft.rodBelowFL100 = rods[0]
aircraft.rodAboveFL100 = rods[1]
aircraft.rodAboveFL240 = rods[2]
return len(speeds) >= 4 and len(rods) >= 3, aircraft
if __name__ == '__main__':
# create the commandline parser
parser = argparse.ArgumentParser(description='Extract the aircraft performace data')
parser.add_argument('directory', help='Directory where to store the performance data configuration')
args = parser.parse_args()
# create the directory if it does not exist
if not os.path.exists(args.directory):
os.makedirs(args.directory)
# parse the aircrafts
links = findAircraftPages('https://www.skybrary.aero', '/index.php?title=Category:Aircraft')
print('Found ' + str(len(links)) + ' aircrafts')
aircrafts = []
parsed = 0
for link in links:
valid, aircraft = parsePerformanceData(link)
parsed += 1
print('Parsed ' + str(parsed) + ' of ' + str(len(links)), end='\r')
if False == valid:
print('Unable to find performance data for ' + link)
continue
aircrafts.append(aircraft)
print('Successfully parsed ' + str(len(aircrafts)) + ' of ' + str(len(links)) + ' aircrafts')
# create the configuration file
config = configparser.ConfigParser()
for aircraft in aircrafts:
config[aircraft.icao] = {
'speedAboveFL240' : aircraft.speedAboveFL240,
'rodAboveFL240' : aircraft.rodAboveFL240,
'speedAboveFL100' : aircraft.speedAboveFL100,
'rodAboveFL100' : aircraft.rodAboveFL100,
'speedBelowFL100' : aircraft.speedBelowFL100,
'rodBelowFL100' : aircraft.rodBelowFL100,
'speedApproach' : aircraft.speedApproach
}
# write the configuration data
with open(args.directory + '/PerformanceData.ini', 'w') as file:
config.write(file)

0
aman/tools/__init__.py Normal file
View File

View File

@@ -0,0 +1,11 @@
#!/usr/bin/env python
class ArrivalRoute:
def __init__(self, name : str, runway : str, waypoints : list):
self.name = name
self.runway = runway
self.iaf = waypoints[0]
self.route = waypoints
def __str__(self):
return 'Name: ' + self.name + ', IAF: ' + self.iaf.name + ', RWY: ' + self.runway

7
aman/types/Inbound.py Normal file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env python
from aman.com import AircraftReport_pb2
class Inbound:
def __init__(self, report : AircraftReport_pb2.AircraftReport):
self.report = report

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env python
class PerformanceData:
def __init__(self, icao : str):
self.icao = icao
self.speedAboveFL240 = 0.0
self.speedAboveFL100 = 0.0
self.speedBelowFL100 = 0.0
self.speedApproach = 0.0
self.rodAboveFL240 = 0.0
self.rodAboveFL100 = 0.0
self.rodBelowFL100 = 2000.0
def __str__(self):
return 'ICAO: ' + self.icao + ', FL240: ' + str(self.rodAboveFL240) + '@' + str(self.speedAboveFL240) + \
', +FL100: ' + str(self.rodAboveFL100) + '@' + str(self.speedAboveFL100) + \
', -FL100: ' + str(self.rodBelowFL100) + '@' + str(self.speedBelowFL100) + \
', Vapp: ' + str(self.speedApproach)

33
aman/types/Waypoint.py Normal file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env python
from sklearn.metrics.pairwise import haversine_distances
import numpy as np
class Waypoint:
def dms2dd(coordinate : str):
split = coordinate.split('.')
if 4 != len(split):
return 0.0
direction = split[0][1]
degrees = float(split[0][1:])
minutes = float(split[1])
seconds = float(split[2]) * (float(split[3]) / 1000.0)
dd = degrees + minutes / 60.0 + seconds / (60 * 60)
if 'E' == direction or 'S' == direction:
dd *= -1.0
return dd
def __init__(self, name : str, latitude : float, longitude : float):
self.name = name
self.coordinate = np.array([ latitude, longitude ])
def __str__(self):
return 'Name: ' + self.name + ', Lat: ' + str(self.coordinate[0]) + ', Lon: ' + str(self.coordinate[1])
def haversine(self, other):
self_radians = [np.radians(_) for _ in self.coordinate]
other_radians = [np.radians(_) for _ in other.coordinate]
return 6371.0 * haversine_distances([self_radians, other_radians])[0][1]

0
aman/types/__init__.py Normal file
View File

BIN
external/bin/protoc.exe vendored Normal file

Binary file not shown.

32
external/licenses/ProtoBuf-3.17.3 vendored Normal file
View File

@@ -0,0 +1,32 @@
Copyright 2008 Google Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Code generated by the Protocol Buffer compiler is owned by the owner
of the input file used when generating it. This code is not
standalone and requires a support library to be linked with it. This
support library is itself covered by the above license.

101
setup.py Normal file
View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python
import os
import re
import shutil
import subprocess
import sys
from setuptools import setup
from distutils.command.clean import clean as _clean
from distutils.command.build_py import build_py_2to3 as _build_py
# @brief Creates Protobuf python files to encode and decode messages
# @param[in] source The protobuf source file
def generateProtobuf(source):
output = source.replace('.proto', '_pb2.py').replace('src/protobuf/', 'aman/com/')
if (not os.path.exists(output) or (os.path.exists(source) and os.path.getmtime(source) > os.path.getmtime(output))):
print('Generating %s...' % output)
if not os.path.exists(source):
sys.stderr.write('Cannot find %s' % source)
sys.exit(-1)
if not os.path.exists('external/bin/protoc.exe'):
sys.stderr.write('Cannot find proto-compiler')
sys.exit(-1)
command = [ 'external/bin/protoc.exe', '-Isrc/protobuf/', '-I.', '--python_out=aman/com/', source]
if 0 != subprocess.call(command):
sys.exit(-1)
# check if we need to replace some import commands
replaced = False
content = open(output, 'r').read()
for entry in re.findall('import.[A-Z].*.as.*', content):
content = content.replace(entry, 'from . ' + entry)
replaced = True
# update the content
if replaced:
with open(output, 'w') as file:
file.write(content)
# @brief Cleans up all auto-generated files and folders
# @param[in] _clean Instance of setuptools to clean up the system
class clean(_clean):
def run(self):
for (dirpath, dirnames, filenames) in os.walk('.'):
for filename in filenames:
filepath = os.path.join(dirpath, filename)
if filepath.endswith('_pb2.py') or filepath.endswith('.pyc'):
os.remove(filepath)
for dirname in dirnames:
if 'Arrival_MANager.egg-info' == dirname or 'build' == dirname or 'dist' == dirname or '__pycache__' == dirname:
shutil.rmtree(os.path.join(dirpath, dirname))
_clean.run(self)
# @brief Generates the python files and folders to set up the development/runtime environment
# @param[in] _build_py Instance of setuptools to build the system
class build_py(_build_py):
def run(self):
generateProtobuf('src/protobuf/Aircraft.proto')
generateProtobuf('src/protobuf/AircraftReport.proto')
generateProtobuf('src/protobuf/AircraftSchedule.proto')
generateProtobuf('src/protobuf/BaseTypes.proto')
_build_py.run(self)
with open('README.md', 'r') as f:
longDescription = f.read()
setup(
name = 'Arrival MANager',
version = '0.1.0',
packages = [
'aman',
'aman.com',
'aman.config',
'aman.formats',
'aman.sys',
'aman.tools',
'aman.types'
],
namespace_packages = [ 'aman' ],
description = 'AMAN optimization backend',
long_description = longDescription,
author = 'Sven Czarnian',
author_email = 'devel@svcz.de',
license = 'GPLv3',
cmdclass = { 'clean': clean, 'build_py': build_py },
install_requires=[
'argparse',
'bs4',
'configparser',
'numpy',
'protobuf',
'pyzmq',
'scikit-learn',
'scipy',
'setuptools'
]
)

1
src/protobuf Submodule

Submodule src/protobuf added at 3c74c96cfd