Compare commits
226 Commits
develop
...
feature/op
| Author | SHA1 | Date | |
|---|---|---|---|
| 3237f20994 | |||
|
|
6426aa2bd4 | ||
|
|
716c1090f3 | ||
|
|
fdf38b46dd | ||
|
|
c9937d39c2 | ||
|
|
357f6e7b11 | ||
|
|
2fdb73e32e | ||
|
|
bf6417c66c | ||
|
|
5ef3d4e40a | ||
|
|
8ac4977c08 | ||
|
|
64fcb8ed01 | ||
|
|
57867a2e21 | ||
|
|
f6736133a7 | ||
|
|
6dc4f6de72 | ||
|
|
50b60915f2 | ||
|
|
857f278afe | ||
|
|
8efed19e34 | ||
|
|
b78e1952ee | ||
|
|
8d196129f0 | ||
|
|
97c7173313 | ||
|
|
b441b945b9 | ||
|
|
d9bc25a507 | ||
|
|
f83febab3a | ||
|
|
4f69df6cc7 | ||
|
|
1b53084e8c | ||
|
|
4439f91633 | ||
|
|
13837fdb62 | ||
|
|
a2ea26ab5d | ||
|
|
83acbdc3b5 | ||
|
|
f4fa9eeb18 | ||
|
|
fc84aab0f2 | ||
|
|
91683ec899 | ||
|
|
8301fba2d2 | ||
|
|
e265629439 | ||
|
|
8ebeef6938 | ||
|
|
e1eb50f6d3 | ||
|
|
7006e8b835 | ||
|
|
dda7a90ef7 | ||
|
|
d1e90cfd22 | ||
|
|
78d61eb6cc | ||
|
|
1acec75d08 | ||
|
|
ae96e5be6b | ||
|
|
bb6cacd898 | ||
|
|
cd5c21d099 | ||
|
|
6fd4f4da1b | ||
|
|
1bf7d850eb | ||
|
|
1e031dcb7c | ||
|
|
cbf033f4b6 | ||
|
|
760145731d | ||
|
|
5242fe0d87 | ||
|
|
72959a8e26 | ||
|
|
5c2765d67a | ||
|
|
bb2aaeba7a | ||
|
|
4f21f8968b | ||
|
|
ad129dc2b6 | ||
|
|
01b12b4398 | ||
|
|
ba34ff97a9 | ||
|
|
a85bcac1e8 | ||
|
|
4fbe9d1060 | ||
|
|
74b8ec33d5 | ||
|
|
52aefd0966 | ||
|
|
4797cef3f7 | ||
|
|
92b7e9e429 | ||
|
|
70be822e2d | ||
|
|
a5cb8914b5 | ||
|
|
0a891b99a5 | ||
|
|
fb40b0aad9 | ||
|
|
9627ae34b7 | ||
|
|
efae307e84 | ||
|
|
7257ab2956 | ||
|
|
9b9746e9bb | ||
|
|
848d89a918 | ||
|
|
6aa2009cce | ||
|
|
22216b6627 | ||
|
|
6d5e635d42 | ||
|
|
50f3e28baf | ||
|
|
f14c6735cc | ||
|
|
2b4200ba58 | ||
|
|
7636c549db | ||
|
|
836ff0a8b2 | ||
|
|
8277721edb | ||
|
|
3746301b0d | ||
|
|
a8419c286f | ||
|
|
fa0a94d733 | ||
|
|
0e96f0402e | ||
|
|
06974b807c | ||
|
|
f359ec8189 | ||
|
|
be6fe84d77 | ||
|
|
3b767489c3 | ||
|
|
5f00ea08cf | ||
|
|
c767572dee | ||
|
|
bd0fdb0899 | ||
|
|
21e79a26f0 | ||
|
|
693f48c535 | ||
|
|
f021baf4cc | ||
|
|
18577ebe9a | ||
|
|
d2b326fa9c | ||
|
|
23d00899fc | ||
|
|
74bf3e7439 | ||
|
|
60d671ea9a | ||
|
|
3affbd9d57 | ||
|
|
3560e98ad2 | ||
|
|
a9fc6bc701 | ||
|
|
7fe1af1def | ||
|
|
09fdf42255 | ||
|
|
03483953c5 | ||
|
|
219ff481c3 | ||
|
|
68dbb0b7da | ||
|
|
8426d7cd30 | ||
|
|
4be95869b0 | ||
|
|
f73a0864de | ||
|
|
0a51874006 | ||
|
|
cae904ee39 | ||
|
|
9bf0600488 | ||
|
|
50cd3e887e | ||
|
|
35d1012bf5 | ||
|
|
9028ef0442 | ||
|
|
e4ce4ff654 | ||
|
|
2d3384f0aa | ||
|
|
7452eb595d | ||
|
|
f7b8f26e48 | ||
|
|
5df1ceb204 | ||
|
|
0a3502e98a | ||
|
|
52d0373ebb | ||
|
|
f74ae4900c | ||
|
|
e1663d7742 | ||
|
|
38b4865ea5 | ||
|
|
8b34f622a3 | ||
|
|
eba9e2deab | ||
|
|
22e9018807 | ||
|
|
bf10649df6 | ||
|
|
b3c98cdcea | ||
|
|
d3a2784ec6 | ||
|
|
9631157b10 | ||
|
|
530c9ea731 | ||
|
|
39dcd03458 | ||
|
|
46e04fca23 | ||
|
|
7cdb04c8fd | ||
|
|
75224a1952 | ||
|
|
8c703c13a1 | ||
|
|
e6fc82fd5a | ||
|
|
cf8ec3242e | ||
|
|
cbbaf0c021 | ||
|
|
d6b85b1660 | ||
|
|
f4da74febd | ||
|
|
d0be115fce | ||
|
|
feb4f85dac | ||
|
|
d1536804a4 | ||
|
|
92d992f92c | ||
|
|
07a447136d | ||
|
|
921919488f | ||
|
|
6f36d8f569 | ||
|
|
40ddd7c188 | ||
|
|
2ef4a485d8 | ||
|
|
63378a347b | ||
|
|
7088bd7bcd | ||
|
|
9cd46dc4cc | ||
|
|
559ab1fa03 | ||
|
|
1a23499f61 | ||
|
|
7985dda3ce | ||
|
|
98285869cd | ||
|
|
f795b301a2 | ||
|
|
7c90ecc3b5 | ||
|
|
f162047767 | ||
|
|
094e0c627a | ||
|
|
b498fe94d1 | ||
|
|
ec019d5006 | ||
|
|
7762cbf213 | ||
|
|
e450b58428 | ||
|
|
014ea5fa0a | ||
|
|
9c9e7dd445 | ||
|
|
5c235f7d2a | ||
|
|
19a9947d3d | ||
|
|
3d87c3918b | ||
|
|
97a2f24f28 | ||
|
|
dd9e725fc2 | ||
|
|
ccb3774872 | ||
|
|
51c963de52 | ||
|
|
7c6d098812 | ||
|
|
d191da2303 | ||
|
|
12d77d0e71 | ||
|
|
33b32befbc | ||
|
|
1b2003e879 | ||
|
|
7a1d4a5959 | ||
|
|
62f2a6c3ed | ||
|
|
01ce0f1bfe | ||
|
|
6151fc255a | ||
|
|
fec26a6d6d | ||
|
|
061eb7eac6 | ||
|
|
a468f1cc53 | ||
|
|
125eef8729 | ||
|
|
c09a5ffe77 | ||
|
|
c835944e8d | ||
|
|
0c97e5aa67 | ||
|
|
43589eaa35 | ||
|
|
a7541925c7 | ||
|
|
3b8989508e | ||
|
|
217c9ad742 | ||
|
|
91b735df2f | ||
|
|
9f6c9f1ff8 | ||
|
|
44837e59f1 | ||
|
|
5295d1c155 | ||
|
|
9887aa48a4 | ||
|
|
73e5a42f52 | ||
|
|
3313a8d463 | ||
|
|
cea52736bf | ||
|
|
d388fb5591 | ||
|
|
2a83754fa4 | ||
|
|
a611a91fe3 | ||
|
|
10d06c2f67 | ||
|
|
17f11a640a | ||
|
|
909cfc9e27 | ||
|
|
014379740b | ||
|
|
1e043e2765 | ||
|
|
9d69a60396 | ||
|
|
7f7506104d | ||
|
|
e5a773cdcb | ||
|
|
a0b00f7c42 | ||
|
|
276e50daa3 | ||
|
|
2ef1e13bd6 | ||
|
|
bba4a75527 | ||
|
|
58d2f5f7f4 | ||
|
|
5e6301f749 | ||
|
|
c07e767ef8 | ||
|
|
e02f429364 | ||
|
|
36ab891f44 |
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,4 +1,4 @@
|
|||||||
[submodule "src/protobuf"]
|
[submodule "src/protobuf"]
|
||||||
path = src/protobuf
|
path = src/protobuf
|
||||||
url = git@git.vatsim-germany.org:nav/aman-com.git
|
url = git@git.ascarion.org:vatger/aman-com.git
|
||||||
branch = feature/protobuf
|
branch = feature/protobuf
|
||||||
|
|||||||
89
aman/AMAN.py
89
aman/AMAN.py
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
import sys
|
import sys
|
||||||
|
from threading import Lock
|
||||||
|
import time
|
||||||
|
|
||||||
from aman.com import AircraftReport_pb2
|
from aman.com import AircraftReport_pb2
|
||||||
from aman.com.Euroscope import Euroscope
|
from aman.com.Euroscope import Euroscope
|
||||||
@@ -27,33 +30,32 @@ class AMAN:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# default initialization of members
|
# default initialization of members
|
||||||
self.systemConfig = None
|
|
||||||
self.aircraftPerformance = None
|
|
||||||
self.receiver = None
|
|
||||||
self.weather = None
|
|
||||||
self.workers = []
|
|
||||||
self.inbounds = {}
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
self.release()
|
|
||||||
|
|
||||||
def aquire(self):
|
|
||||||
configPath = AMAN.findConfigPath()
|
configPath = AMAN.findConfigPath()
|
||||||
|
self.SystemConfig = None
|
||||||
|
self.AircraftPerformance = None
|
||||||
|
self.Receiver = None
|
||||||
|
self.Weather = None
|
||||||
|
self.WebUi = None
|
||||||
|
self.Workers = []
|
||||||
|
self.WorkersLock = Lock()
|
||||||
|
|
||||||
# read all system relevant configuration files
|
# read all system relevant configuration files
|
||||||
self.systemConfig = System(os.path.join(configPath, 'System.ini'))
|
self.SystemConfig = System(os.path.join(configPath, 'System.ini'))
|
||||||
print('Parsed System.ini')
|
print('Parsed System.ini')
|
||||||
|
|
||||||
# read the aircraft performance data
|
# read the aircraft performance data
|
||||||
self.aircraftPerformance = AircraftPerformance(os.path.join(configPath, 'PerformanceData.ini'))
|
self.AircraftPerformance = AircraftPerformance(os.path.join(configPath, 'PerformanceData.ini'))
|
||||||
if None == self.aircraftPerformance:
|
if None == self.AircraftPerformance:
|
||||||
sys.stderr.write('No aircraft performance data found!')
|
sys.stderr.write('No aircraft performance data found!')
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
else:
|
else:
|
||||||
print('Parsed PerformanceData.ini. Extracted ' + str(len(self.aircraftPerformance.aircrafts)) + ' aircrafts')
|
print('Parsed PerformanceData.ini. Extracted ' + str(len(self.AircraftPerformance.Aircrafts)) + ' aircrafts')
|
||||||
|
|
||||||
self.weather = Weather()
|
# create the communication syb
|
||||||
self.weather.acquire(self.systemConfig.Weather)
|
self.Weather = Weather(self.SystemConfig.Weather)
|
||||||
|
self.Receiver = Euroscope(configPath, self.SystemConfig.Server, self)
|
||||||
|
|
||||||
|
self.acquireLock()
|
||||||
|
|
||||||
# find the airport configurations and create the workers
|
# find the airport configurations and create the workers
|
||||||
airportsPath = os.path.join(os.path.join(configPath, 'airports'), '*.ini')
|
airportsPath = os.path.join(os.path.join(configPath, 'airports'), '*.ini')
|
||||||
@@ -64,34 +66,45 @@ class AMAN:
|
|||||||
airportConfig = Airport(file, icao)
|
airportConfig = Airport(file, icao)
|
||||||
|
|
||||||
# initialize the worker thread
|
# initialize the worker thread
|
||||||
worker = Worker()
|
worker = Worker(icao, airportConfig, self.Weather, self.AircraftPerformance, self.Receiver)
|
||||||
worker.acquire(icao, airportConfig)
|
self.Workers.append(worker)
|
||||||
self.workers.append(worker)
|
|
||||||
print('Started worker for ' + icao)
|
print('Started worker for ' + icao)
|
||||||
|
|
||||||
# create the EuroScope receiver
|
self.releaseLock()
|
||||||
self.receiver = Euroscope()
|
|
||||||
self.receiver.acquire(configPath, self.systemConfig.Server, self)
|
|
||||||
|
|
||||||
def release(self):
|
# initialize the random number generator
|
||||||
if None != self.receiver:
|
random.seed(time.time())
|
||||||
self.receiver.release()
|
|
||||||
self.receiver = None
|
|
||||||
|
|
||||||
if None != self.weather:
|
def acquireLock(self):
|
||||||
self.weather.release()
|
if None != self.WorkersLock:
|
||||||
self.weather = None
|
self.WorkersLock.acquire()
|
||||||
|
|
||||||
if None != self.workers:
|
def releaseLock(self):
|
||||||
for worker in self.workers:
|
if None != self.WorkersLock:
|
||||||
worker.release()
|
self.WorkersLock.release()
|
||||||
self.workers = None
|
|
||||||
|
|
||||||
def updateAircraftReport(self, report : AircraftReport_pb2.AircraftReport):
|
def updateAircraftReport(self, report : AircraftReport_pb2.AircraftReport):
|
||||||
|
self.acquireLock()
|
||||||
|
|
||||||
# find the correct worker for the inbound
|
# find the correct worker for the inbound
|
||||||
for worker in self.workers:
|
for worker in self.Workers:
|
||||||
if worker.icao == report.destination:
|
if worker.Icao == report.destination:
|
||||||
worker.acquireLock()
|
worker.acquireLock()
|
||||||
worker.reportQueue[report.aircraft.callsign] = report
|
worker.ReportQueue[report.aircraft.callsign] = report
|
||||||
worker.releaseLock()
|
worker.releaseLock()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
self.releaseLock()
|
||||||
|
|
||||||
|
def findAirport(self, icao : str):
|
||||||
|
self.acquireLock()
|
||||||
|
|
||||||
|
airport = None
|
||||||
|
for worker in self.Workers:
|
||||||
|
if icao == worker.Icao:
|
||||||
|
airport = worker
|
||||||
|
break
|
||||||
|
|
||||||
|
self.releaseLock()
|
||||||
|
|
||||||
|
return airport
|
||||||
|
|||||||
1
aman/VERSION
Normal file
1
aman/VERSION
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0.1.0
|
||||||
230
aman/app.py
Normal file
230
aman/app.py
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from subprocess import Popen, PIPE
|
||||||
|
|
||||||
|
from flask import Flask, Response, request
|
||||||
|
from flask_cors import CORS, cross_origin
|
||||||
|
from json import JSONEncoder
|
||||||
|
|
||||||
|
from aman.AMAN import AMAN
|
||||||
|
from aman.config.AirportSequencing import AirportSequencing
|
||||||
|
from aman.config.RunwaySequencing import RunwaySequencing
|
||||||
|
|
||||||
|
class InboundEncoder(JSONEncoder):
|
||||||
|
def default(self, o):
|
||||||
|
if None == o.PlannedArrivalTime or None == o.EnrouteArrivalTime or None == o.PlannedRunway:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# configure the PTA
|
||||||
|
pta = str(o.PlannedArrivalTime)
|
||||||
|
delimiter = pta.find('.')
|
||||||
|
if -1 == delimiter:
|
||||||
|
delimiter = pta.find('+')
|
||||||
|
|
||||||
|
# calculate the delta time
|
||||||
|
delta = int((o.PlannedArrivalTime - o.EnrouteArrivalTime).total_seconds() / 60.0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
'callsign' : o.Callsign,
|
||||||
|
'fixed' : o.FixedSequence,
|
||||||
|
'runway' : o.PlannedRunway.Name,
|
||||||
|
'pta' : pta[0:delimiter],
|
||||||
|
'delay' : delta,
|
||||||
|
'wtc' : o.WTC,
|
||||||
|
'iaf' : o.Report.initialApproachFix,
|
||||||
|
'expectedrunway' : o.ExpectedRunway,
|
||||||
|
'assignmentmode' : o.AssignmentMode
|
||||||
|
}
|
||||||
|
|
||||||
|
class RunwaySequencingEncoder(JSONEncoder):
|
||||||
|
def default(self, o):
|
||||||
|
return { 'runway' : o.Runway.Name, 'spacing' : o.Spacing }
|
||||||
|
|
||||||
|
# initialize the environment variables
|
||||||
|
if 'AMAN_PATH' not in os.environ:
|
||||||
|
os.environ['AMAN_PATH'] = 'C:\\Repositories\VATSIM\\AMAN\\aman-sys\\aman'
|
||||||
|
if 'AMAN_CONFIG_PATH' not in os.environ:
|
||||||
|
os.environ['AMAN_CONFIG_PATH'] = 'C:\\Repositories\\VATSIM\\AMAN\\config'
|
||||||
|
|
||||||
|
# initialize the AMAN and the interface version
|
||||||
|
aman = AMAN()
|
||||||
|
version = '0.0.0'
|
||||||
|
with open(os.path.join(os.environ['AMAN_PATH'], 'VERSION')) as file:
|
||||||
|
version = file.readline()
|
||||||
|
|
||||||
|
# initialize the web services
|
||||||
|
app = Flask('AMAN')
|
||||||
|
cors = CORS(app)
|
||||||
|
app.config['CORS_HEADERS'] = 'Content-Type'
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run()
|
||||||
|
|
||||||
|
@app.route('/aman/airports')
|
||||||
|
@cross_origin()
|
||||||
|
def airports():
|
||||||
|
# get the airports
|
||||||
|
retval = []
|
||||||
|
for airport in aman.Workers:
|
||||||
|
retval.append(airport.Icao)
|
||||||
|
|
||||||
|
data = json.dumps({ 'version' : version, 'airports' : retval }, ensure_ascii=True)
|
||||||
|
return Response(data, status=200, mimetype='application/json')
|
||||||
|
|
||||||
|
@app.route('/aman/admin/newuser')
|
||||||
|
def newUser():
|
||||||
|
toolpath = os.path.join(os.path.join(os.environ['AMAN_PATH'], 'tools'), 'KeyPairCreator.py')
|
||||||
|
serverKeypath = os.path.join(os.path.join(os.path.join(AMAN.findConfigPath(), 'keys'), 'server'), 'server.key')
|
||||||
|
clientKeypath = os.path.join(os.path.join(AMAN.findConfigPath(), 'keys'), 'clients')
|
||||||
|
|
||||||
|
cmd = ['python', toolpath, '--directory=' + clientKeypath, '--publickey=' + serverKeypath]
|
||||||
|
|
||||||
|
child = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||||
|
stdout, _ = child.communicate()
|
||||||
|
if 0 != child.returncode:
|
||||||
|
return Response('{}', status=404, mimetype='application/json')
|
||||||
|
|
||||||
|
keys = stdout.splitlines()
|
||||||
|
server = keys[0].decode('ascii')
|
||||||
|
public = keys[1].decode('ascii')
|
||||||
|
private = keys[2].decode('ascii')
|
||||||
|
|
||||||
|
dictionary = {
|
||||||
|
'server' : server,
|
||||||
|
'public' : public,
|
||||||
|
'private' : private,
|
||||||
|
}
|
||||||
|
data = json.dumps(dictionary, ensure_ascii=True)
|
||||||
|
return Response(data, status=200, mimetype='application/json')
|
||||||
|
|
||||||
|
@app.route('/aman/configuration/<icao>')
|
||||||
|
@cross_origin()
|
||||||
|
def configuration(icao):
|
||||||
|
airport = aman.findAirport(icao.upper())
|
||||||
|
if None == airport:
|
||||||
|
return Response('{}', status=404, mimetype='application/json')
|
||||||
|
|
||||||
|
# get the current runway configuration
|
||||||
|
config = airport.SequencingConfiguration
|
||||||
|
dependencies = []
|
||||||
|
for dependency in config.RunwayDependencies:
|
||||||
|
rwy0 = config.runway(dependency[0])
|
||||||
|
rwy1 = config.runway(dependency[1])
|
||||||
|
cand1 = [ rwy0.Name, rwy1.Name ]
|
||||||
|
cand2 = [ rwy1.Name, rwy0.Name ]
|
||||||
|
found = False
|
||||||
|
|
||||||
|
for dep in dependencies:
|
||||||
|
if cand1 == dep or cand2 == dep:
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if False == found:
|
||||||
|
dependencies.append(cand1)
|
||||||
|
|
||||||
|
runways = airport.Configuration.GngData.Runways[airport.Icao];
|
||||||
|
availableRunways = [];
|
||||||
|
for runway in runways:
|
||||||
|
availableRunways.append(runway.Name);
|
||||||
|
|
||||||
|
# get all IAFs of the airport
|
||||||
|
iafs = []
|
||||||
|
for runway in airport.Configuration.GngData.ArrivalRoutes:
|
||||||
|
for route in airport.Configuration.GngData.ArrivalRoutes[runway]:
|
||||||
|
found = False
|
||||||
|
for iaf in iafs:
|
||||||
|
if iaf['name'] == route.Iaf.Name:
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if False == found:
|
||||||
|
iafs.append({ 'name' : route.Iaf.Name, 'lat' : route.Iaf.Coordinate[0], 'lon' : route.Iaf.Coordinate[1] })
|
||||||
|
|
||||||
|
dictionary = {
|
||||||
|
'airport' : airport.Icao,
|
||||||
|
'useShallShouldMay' : config.UseShallShouldMay,
|
||||||
|
'availableRunways' : availableRunways,
|
||||||
|
'activeRunways' : config.ActiveArrivalRunways,
|
||||||
|
'dependentRunways' : dependencies,
|
||||||
|
'iafColorization' : airport.Configuration.IafColorization,
|
||||||
|
'iafs' : iafs
|
||||||
|
}
|
||||||
|
data = json.dumps(dictionary, ensure_ascii=True, cls=RunwaySequencingEncoder)
|
||||||
|
return Response(data, status=200, mimetype='application/json')
|
||||||
|
|
||||||
|
@app.route('/aman/sequence/<icao>')
|
||||||
|
@cross_origin()
|
||||||
|
def sequence(icao):
|
||||||
|
airport = aman.findAirport(icao.upper())
|
||||||
|
if None == airport:
|
||||||
|
return Response('{}', status=404, mimetype='application/json')
|
||||||
|
|
||||||
|
# convert the timestamp
|
||||||
|
stamp = str(airport.SequencingConfiguration.LastUpdateTimestamp)
|
||||||
|
delimiter = stamp.find('.')
|
||||||
|
if -1 == delimiter:
|
||||||
|
delimiter = stamp.find('+')
|
||||||
|
|
||||||
|
dictionary = {
|
||||||
|
'airport': airport.Icao,
|
||||||
|
'lastConfigurationUpdate': stamp[0:delimiter],
|
||||||
|
'sequence': airport.inboundSequence()
|
||||||
|
}
|
||||||
|
data = json.dumps(dictionary, ensure_ascii=True, cls=InboundEncoder)
|
||||||
|
return Response(data, status=200, mimetype='application/json')
|
||||||
|
|
||||||
|
@app.route('/aman/configure', methods=['POST'])
|
||||||
|
@cross_origin()
|
||||||
|
def configure():
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
# validate that the airport exists
|
||||||
|
if 'airport' not in data:
|
||||||
|
return Response('{}', status=404, mimetype='application/json')
|
||||||
|
airport = aman.findAirport(data['airport'].upper())
|
||||||
|
if None == airport:
|
||||||
|
return Response('{}', status=404, mimetype='application/json')
|
||||||
|
|
||||||
|
# check that all top-level information are available
|
||||||
|
if 'useShallShouldMay' not in data or 'activeRunways' not in data or 'dependentRunways' not in data:
|
||||||
|
return Response('{}', status=404, mimetype='application/json')
|
||||||
|
if False == isinstance(data['useShallShouldMay'], bool) or 0 == len(data['activeRunways']):
|
||||||
|
return Response('{}', status=404, mimetype='application/json')
|
||||||
|
|
||||||
|
# create the toplevel information
|
||||||
|
config = AirportSequencing(airport.Icao)
|
||||||
|
config.Airport = data['airport'].upper()
|
||||||
|
config.UseShallShouldMay = data['useShallShouldMay']
|
||||||
|
|
||||||
|
# parse the active runways
|
||||||
|
for activeRunway in data['activeRunways']:
|
||||||
|
if 'runway' not in activeRunway or 'spacing' not in activeRunway:
|
||||||
|
return Response('{}', status=404, mimetype='application/json')
|
||||||
|
if False == isinstance(activeRunway['runway'], str) or False == isinstance(activeRunway['spacing'], int):
|
||||||
|
return Response('{}', status=404, mimetype='application/json')
|
||||||
|
|
||||||
|
gngRunway = None
|
||||||
|
for runway in airport.Configuration.GngData.Runways[airport.Icao]:
|
||||||
|
if runway.Name == activeRunway['runway']:
|
||||||
|
gngRunway = runway
|
||||||
|
break
|
||||||
|
|
||||||
|
# could not find the runway
|
||||||
|
if None == gngRunway:
|
||||||
|
return None
|
||||||
|
|
||||||
|
runway = RunwaySequencing(gngRunway)
|
||||||
|
runway.Spacing = activeRunway['spacing']
|
||||||
|
config.activateRunway(runway)
|
||||||
|
|
||||||
|
# parse the dependent runways
|
||||||
|
for dependency in data['dependentRunways']:
|
||||||
|
if 2 != len(dependency) or False == isinstance(dependency[0], str) or False == isinstance(dependency[1], str):
|
||||||
|
return Response('{}', status=404, mimetype='application/json')
|
||||||
|
if False == config.addDependency(dependency[0], dependency[1]):
|
||||||
|
return Response('{}', status=404, mimetype='application/json')
|
||||||
|
|
||||||
|
airport.Configuration.assignmentUpdate(config)
|
||||||
|
airport.configure(config)
|
||||||
|
|
||||||
|
return Response('{}', status=200, mimetype='application/json')
|
||||||
@@ -19,8 +19,8 @@ from datetime import datetime as dt
|
|||||||
# - third element of wind data tuple: wind speed (KT)
|
# - third element of wind data tuple: wind speed (KT)
|
||||||
class DwdCrawler():
|
class DwdCrawler():
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.updateTime = None
|
self.UpdateTime = None
|
||||||
self.windData = None
|
self.WindData = None
|
||||||
|
|
||||||
def parseGaforAreas(areas : str):
|
def parseGaforAreas(areas : str):
|
||||||
areas = areas.replace(':', '')
|
areas = areas.replace(':', '')
|
||||||
@@ -57,7 +57,10 @@ class DwdCrawler():
|
|||||||
altitude = int(altitude.replace('FL', '')) * 100
|
altitude = int(altitude.replace('FL', '')) * 100
|
||||||
else:
|
else:
|
||||||
altitude = int(altitude.replace('FT', ''))
|
altitude = int(altitude.replace('FT', ''))
|
||||||
row = ( altitude, int(windData[0]), int(windData[1].replace('KT', '')) )
|
if 'VRB' == windData[0]:
|
||||||
|
row = ( altitude, 0, int(windData[1].replace('KT', '')) )
|
||||||
|
else:
|
||||||
|
row = ( altitude, int(windData[0]), int(windData[1].replace('KT', '')) )
|
||||||
table.append(row)
|
table.append(row)
|
||||||
|
|
||||||
return table
|
return table
|
||||||
@@ -107,7 +110,8 @@ class DwdCrawler():
|
|||||||
for line in content.splitlines():
|
for line in content.splitlines():
|
||||||
if '' == line:
|
if '' == line:
|
||||||
if 0 != len(windTable):
|
if 0 != len(windTable):
|
||||||
windInformation.append(( areaIds, windTable ))
|
for id in areaIds:
|
||||||
|
windInformation.append([ id, windTable ])
|
||||||
areaIds = None
|
areaIds = None
|
||||||
windTable = []
|
windTable = []
|
||||||
elif line.startswith('GAFOR-Gebiete'):
|
elif line.startswith('GAFOR-Gebiete'):
|
||||||
@@ -125,8 +129,8 @@ class DwdCrawler():
|
|||||||
return nextUpdate, windInformation
|
return nextUpdate, windInformation
|
||||||
|
|
||||||
def receiveWindData(self):
|
def receiveWindData(self):
|
||||||
self.updateTime = None
|
self.UpdateTime = None
|
||||||
self.windData = None
|
self.WindData = None
|
||||||
|
|
||||||
with urllib.request.urlopen('https://www.dwd.de/DE/fachnutzer/luftfahrt/teaser/luftsportberichte/luftsportberichte_node.html') as site:
|
with urllib.request.urlopen('https://www.dwd.de/DE/fachnutzer/luftfahrt/teaser/luftsportberichte/luftsportberichte_node.html') as site:
|
||||||
data = site.read().decode('utf-8')
|
data = site.read().decode('utf-8')
|
||||||
@@ -141,17 +145,18 @@ class DwdCrawler():
|
|||||||
pages.append('https://www.dwd.de/' + link['href'].split(';')[0])
|
pages.append('https://www.dwd.de/' + link['href'].split(';')[0])
|
||||||
|
|
||||||
# receive the wind data
|
# receive the wind data
|
||||||
self.updateTime = None
|
self.UpdateTime = None
|
||||||
self.windData = []
|
self.WindData = {}
|
||||||
for page in pages:
|
for page in pages:
|
||||||
next, wind = self.parseGaforPage(page)
|
next, wind = self.parseGaforPage(page)
|
||||||
if None != next:
|
if None != next:
|
||||||
if None == self.updateTime or self.updateTime > next:
|
if None == self.UpdateTime or self.UpdateTime > next:
|
||||||
self.updateTime = next
|
self.UpdateTime = next
|
||||||
self.windData.extend(wind)
|
for gafor in wind:
|
||||||
|
self.WindData[gafor[0]] = gafor[1]
|
||||||
|
|
||||||
# indicate that new wind data is available
|
# indicate that new wind data is available
|
||||||
if None != self.updateTime:
|
if None != self.UpdateTime:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
import ctypes
|
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@@ -9,63 +8,49 @@ import time
|
|||||||
import zmq
|
import zmq
|
||||||
import zmq.auth
|
import zmq.auth
|
||||||
|
|
||||||
from aman.com import AircraftReport_pb2
|
from aman.com import Communication_pb2
|
||||||
from aman.config.Server import Server
|
from aman.config.Server import Server
|
||||||
from threading import Thread, _active
|
from threading import Thread
|
||||||
|
|
||||||
class ReceiverThread(Thread):
|
class ComThread(Thread):
|
||||||
def __init__(self, socket, aman):
|
def __init__(self, com, aman):
|
||||||
Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
self.socket = socket
|
self.Com = com
|
||||||
self.aman = aman
|
self.AMAN = aman
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
msg = self.socket.recv(zmq.NOBLOCK)
|
msg = self.Com.Socket.recv(zmq.NOBLOCK)
|
||||||
|
|
||||||
# parse the received message
|
# parse the received message
|
||||||
report = AircraftReport_pb2.AircraftReport()
|
report = Communication_pb2.AircraftUpdate()
|
||||||
report.ParseFromString(msg)
|
report.ParseFromString(msg)
|
||||||
|
|
||||||
# try to associate the received aircraft to an airport
|
# try to associate the received aircrafts to airports
|
||||||
self.aman.updateAircraftReport(report)
|
for inbound in report.reports:
|
||||||
|
self.AMAN.updateAircraftReport(inbound)
|
||||||
|
|
||||||
|
# get the sequence of the airport
|
||||||
|
if None != report.airport:
|
||||||
|
airport = self.AMAN.findAirport(report.airport)
|
||||||
|
if None != airport:
|
||||||
|
self.Com.sendSequence(report.airport, airport.inboundSequence(), airport.WeatherModel)
|
||||||
|
|
||||||
except zmq.ZMQError as error:
|
except zmq.ZMQError as error:
|
||||||
if zmq.EAGAIN == error.errno:
|
if zmq.EAGAIN == error.errno:
|
||||||
time.sleep(0.5)
|
time.sleep(0.1)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
def threadId(self):
|
|
||||||
if hasattr(self, '_thread_id'):
|
|
||||||
return self._thread_id
|
|
||||||
for id, thread in _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
|
# @brief Receives and sends messages to EuroScope plugins
|
||||||
class Euroscope:
|
class Euroscope:
|
||||||
def __init__(self):
|
def __init__(self, configPath : str, config : Server, aman):
|
||||||
self.context = None
|
self.Context = None
|
||||||
self.receiverSocket = None
|
self.Socket = None
|
||||||
self.receiverThread = None
|
self.Thread = None
|
||||||
self.notificationSocket = None
|
self.Context = zmq.Context()
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
self.release()
|
|
||||||
|
|
||||||
# @brief Initializes the ZMQ socket
|
|
||||||
# @param[in] config The server configuration
|
|
||||||
def acquire(self, configPath : str, config : Server, aman):
|
|
||||||
self.context = zmq.Context()
|
|
||||||
|
|
||||||
# find the key directories
|
# find the key directories
|
||||||
serverKeyPath = os.path.join(os.path.join(configPath, 'keys'), 'server')
|
serverKeyPath = os.path.join(os.path.join(configPath, 'keys'), 'server')
|
||||||
@@ -88,34 +73,62 @@ class Euroscope:
|
|||||||
keyPair = zmq.auth.load_certificate(keyPairPath[0])
|
keyPair = zmq.auth.load_certificate(keyPairPath[0])
|
||||||
|
|
||||||
# initialize the receiver
|
# initialize the receiver
|
||||||
self.receiverSocket = zmq.Socket(self.context, zmq.SUB)
|
self.Socket = zmq.Socket(self.Context, zmq.REP)
|
||||||
self.receiverSocket.setsockopt(zmq.CURVE_PUBLICKEY, keyPair[0])
|
self.Socket.setsockopt(zmq.CURVE_PUBLICKEY, keyPair[0])
|
||||||
self.receiverSocket.setsockopt(zmq.CURVE_SECRETKEY, keyPair[1])
|
self.Socket.setsockopt(zmq.CURVE_SECRETKEY, keyPair[1])
|
||||||
self.receiverSocket.setsockopt(zmq.CURVE_SERVER, True)
|
self.Socket.setsockopt(zmq.CURVE_SERVER, True)
|
||||||
self.receiverSocket.bind('tcp://' + config.Address + ':' + str(config.PortReceiver))
|
self.Socket.bind('tcp://' + config.Address + ':' + str(config.PortReceiver))
|
||||||
self.receiverSocket.setsockopt(zmq.SUBSCRIBE, b'')
|
#self.Socket.setsockopt(zmq.SUBSCRIBE, b'')
|
||||||
self.receiverThread = ReceiverThread(self.receiverSocket, aman)
|
self.Thread = ComThread(self, aman)
|
||||||
self.receiverThread.start()
|
self.Thread.setDaemon(True)
|
||||||
|
self.Thread.start()
|
||||||
print('Listening to tcp://' + config.Address + ':' + str(config.PortReceiver))
|
print('Listening to tcp://' + config.Address + ':' + str(config.PortReceiver))
|
||||||
|
|
||||||
# initialize the notification
|
def sendSequence(self, airport : str, inbounds, weather):
|
||||||
self.notificationSocket = zmq.Socket(self.context, zmq.PUB)
|
if None == self.Socket:
|
||||||
self.notificationSocket.setsockopt(zmq.CURVE_PUBLICKEY, keyPair[0])
|
return
|
||||||
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 to tcp://' + config.Address + ':' + str(config.PortNotification))
|
|
||||||
|
|
||||||
def release(self):
|
sequence = Communication_pb2.AircraftSequence()
|
||||||
if None != self.receiverThread:
|
sequence.airport = airport
|
||||||
self.receiverThread.stopThread()
|
|
||||||
self.receiverThread.join()
|
|
||||||
self.receiverThread = None
|
|
||||||
|
|
||||||
if None != self.receiverSocket:
|
# convert the wind data
|
||||||
self.receiverSocket.close()
|
if None != weather.Altitudes:
|
||||||
self.receiverSocket = None
|
for i in range(0, len(weather.Altitudes)):
|
||||||
|
entry = sequence.windData.add()
|
||||||
|
entry.altitude = int(weather.Altitudes[i])
|
||||||
|
entry.direction = int(weather.Directions[i])
|
||||||
|
entry.speed = int(weather.Windspeeds[i])
|
||||||
|
|
||||||
if None != self.notificationSocket:
|
# convert the inbound sequence
|
||||||
self.notificationSocket.close()
|
for inbound in inbounds:
|
||||||
self.notificationSocket = None
|
entry = sequence.sequence.add()
|
||||||
|
entry.callsign = inbound.Callsign
|
||||||
|
entry.fixed = inbound.FixedSequence
|
||||||
|
if None != inbound.PlannedStar:
|
||||||
|
entry.arrivalRoute = inbound.PlannedStar.Name
|
||||||
|
if None != inbound.PlannedRunway:
|
||||||
|
entry.arrivalRunway = inbound.PlannedRunway.Name
|
||||||
|
|
||||||
|
if None != inbound.PerformanceData:
|
||||||
|
entry.performance.iasAboveFL240 = int(round(inbound.PerformanceData.SpeedAboveFL240))
|
||||||
|
entry.performance.iasAboveFL100 = int(round(inbound.PerformanceData.SpeedAboveFL100))
|
||||||
|
entry.performance.iasBelowFL100 = int(round(inbound.PerformanceData.SpeedBelowFL100))
|
||||||
|
entry.performance.iasApproach = int(round(inbound.PerformanceData.SpeedApproach))
|
||||||
|
|
||||||
|
if None != inbound.PlannedArrivalRoute:
|
||||||
|
for waypoint in inbound.PlannedArrivalRoute:
|
||||||
|
wp = entry.waypoints.add()
|
||||||
|
wp.name = waypoint.Waypoint.Name
|
||||||
|
wp.altitude = int(round(waypoint.Altitude))
|
||||||
|
wp.indicatedAirspeed = int(round(waypoint.IndicatedAirspeed))
|
||||||
|
wp.groundSpeed = int(round(waypoint.GroundSpeed))
|
||||||
|
|
||||||
|
pta = str(waypoint.PTA)
|
||||||
|
delimiter = pta.find('.')
|
||||||
|
if -1 == delimiter:
|
||||||
|
delimiter = pta.find('+')
|
||||||
|
|
||||||
|
wp.pta = pta[0:delimiter]
|
||||||
|
|
||||||
|
message = sequence.SerializeToString()
|
||||||
|
self.Socket.send(message)
|
||||||
|
|||||||
@@ -11,46 +11,37 @@ from aman.com.DwdCrawler import DwdCrawler
|
|||||||
import aman.config.Weather
|
import aman.config.Weather
|
||||||
|
|
||||||
class Weather(Thread):
|
class Weather(Thread):
|
||||||
def __init__(self):
|
def __init__(self, config : aman.config.Weather.Weather):
|
||||||
Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
|
|
||||||
self.nextUpdate = None
|
self.NextUpdate = dt.utcfromtimestamp(int(time.time()))
|
||||||
self.lastUpdateTried = None
|
self.LastUpdateTried = None
|
||||||
self.stopThread = False
|
self.StopThread = False
|
||||||
self.provider = None
|
self.Provider = None
|
||||||
|
|
||||||
def acquire(self, config : aman.config.Weather.Weather):
|
|
||||||
self.nextUpdate = dt.utcfromtimestamp(int(time.time()))
|
|
||||||
self.lastUpdateTried = None
|
|
||||||
self.stopThread = False
|
|
||||||
self.provider = None
|
|
||||||
|
|
||||||
if 'DWD' == config.Provider.upper():
|
if 'DWD' == config.Provider.upper():
|
||||||
self.provider = DwdCrawler()
|
self.Provider = DwdCrawler()
|
||||||
else:
|
elif 'NONE' != config.Provider.upper():
|
||||||
sys.stderr.write('Invalid or unknown weather-provider defined')
|
sys.stderr.write('Invalid or unknown weather-provider defined')
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
|
self.setDaemon(True)
|
||||||
self.start()
|
self.start()
|
||||||
|
|
||||||
def release(self):
|
|
||||||
self.stopThread = True
|
|
||||||
self.join()
|
|
||||||
|
|
||||||
def currentClock():
|
def currentClock():
|
||||||
clock = dt.utcfromtimestamp(int(time.time())).replace(tzinfo = pytz.UTC)
|
clock = dt.utcfromtimestamp(int(time.time())).replace(tzinfo = pytz.UTC)
|
||||||
return clock
|
return clock
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while False == self.stopThread and None != self.provider:
|
while False == self.StopThread and None != self.Provider:
|
||||||
now = Weather.currentClock()
|
now = Weather.currentClock()
|
||||||
|
|
||||||
# check if an update is required
|
# check if an update is required
|
||||||
if None != self.provider.updateTime and self.provider.updateTime > now:
|
if None != self.Provider.UpdateTime and self.Provider.UpdateTime > now:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if None == self.lastUpdateTried or self.lastUpdateTried <= now:
|
if None == self.LastUpdateTried or self.LastUpdateTried <= now:
|
||||||
if True == self.provider.receiveWindData():
|
if True == self.Provider.receiveWindData():
|
||||||
self.nextUpdate = self.provider.updateTime
|
self.NextUpdate = self.Provider.UpdateTime
|
||||||
print('Received new wind data')
|
print('Received new wind data')
|
||||||
@@ -8,7 +8,7 @@ class AircraftPerformance:
|
|||||||
def __init__(self, filepath : str):
|
def __init__(self, filepath : str):
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
config.read(filepath)
|
config.read(filepath)
|
||||||
self.aircrafts = { }
|
self.Aircrafts = { }
|
||||||
|
|
||||||
# iterate over all entries
|
# iterate over all entries
|
||||||
for key in config:
|
for key in config:
|
||||||
@@ -17,12 +17,12 @@ class AircraftPerformance:
|
|||||||
|
|
||||||
aircraft = PerformanceData(key)
|
aircraft = PerformanceData(key)
|
||||||
|
|
||||||
aircraft.speedAboveFL240 = config[key]['speedabovefl240']
|
aircraft.SpeedAboveFL240 = float(config[key]['speedabovefl240'])
|
||||||
aircraft.rodAboveFL240 = config[key]['rodabovefl240']
|
aircraft.RodAboveFL240 = float(config[key]['rodabovefl240'])
|
||||||
aircraft.speedAboveFL100 = config[key]['speedabovefl100']
|
aircraft.SpeedAboveFL100 = float(config[key]['speedabovefl100'])
|
||||||
aircraft.rodAboveFL100 = config[key]['rodabovefl100']
|
aircraft.RodAboveFL100 = float(config[key]['rodabovefl100'])
|
||||||
aircraft.speedBelowFL100 = config[key]['speedbelowfl100']
|
aircraft.SpeedBelowFL100 = float(config[key]['speedbelowfl100'])
|
||||||
aircraft.rodBelowFL100 = config[key]['rodbelowfl100']
|
aircraft.RodBelowFL100 = float(config[key]['rodbelowfl100'])
|
||||||
aircraft.speedApproach = config[key]['speedapproach']
|
aircraft.SpeedApproach = float(config[key]['speedapproach'])
|
||||||
|
|
||||||
self.aircrafts[aircraft.icao] = aircraft
|
self.Aircrafts[aircraft.Icao] = aircraft
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
import configparser
|
import configparser
|
||||||
|
from datetime import timedelta
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from formats.SctEseFormat import SctEseFormat
|
from aman.config.RHC import RHC
|
||||||
|
from aman.config.AirportSequencing import AirportSequencing
|
||||||
|
from aman.config.RunwaySequencing import RunwaySequencing, RunwayAssignmentType
|
||||||
|
from aman.formats.SctEseFormat import SctEseFormat
|
||||||
|
from aman.types.Waypoint import Waypoint
|
||||||
|
|
||||||
class Airport:
|
class Airport:
|
||||||
def findGngData(data, path):
|
def findGngData(data, path):
|
||||||
@@ -30,14 +35,282 @@ class Airport:
|
|||||||
return []
|
return []
|
||||||
return planning['routes'].split(':')
|
return planning['routes'].split(':')
|
||||||
|
|
||||||
def __init__(self, filepath : str, icao : str):
|
def parseDefaultSequencingConfiguration(self, icao : str, planning):
|
||||||
self.arrivalRoutes = {}
|
if None == planning.get('activearrivalrunwaydefault'):
|
||||||
|
sys.stderr.write('No "activearrivalrunwaydefault" entry found!')
|
||||||
|
sys.exit(-1)
|
||||||
|
if None == planning.get('activearrivalmodedefault'):
|
||||||
|
sys.stderr.write('No "activearrivalmodedefault" entry found!')
|
||||||
|
sys.exit(-1)
|
||||||
|
if None == planning.get('arrivalspacingdefault'):
|
||||||
|
sys.stderr.write('No "arrivalspacingdefault" entry found!')
|
||||||
|
sys.exit(-1)
|
||||||
|
if not icao in self.GngData.Runways:
|
||||||
|
sys.stderr.write('Unable to find' + icao + 'in the SCT data!')
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
# parse the default arrival mode
|
||||||
|
if 'STAGGERED' == planning['activearrivalmodedefault']:
|
||||||
|
staggered = True
|
||||||
|
elif 'IPA' == planning['activearrivalmodedefault']:
|
||||||
|
staggered = False
|
||||||
|
else:
|
||||||
|
sys.stderr.write('Unknown arrival mode in "" found! (STAGGERED or IPA needs to be set)')
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
# translate the spacing into a map
|
||||||
|
ident = ''
|
||||||
|
spacings = {}
|
||||||
|
spacingConfig = list(filter(None, planning['arrivalspacingdefault'].split(':')))
|
||||||
|
for i in range(0, len(spacingConfig)):
|
||||||
|
if 0 == i % 2:
|
||||||
|
ident = spacingConfig[i]
|
||||||
|
elif '' != ident:
|
||||||
|
spacings[ident] = int(spacingConfig[i])
|
||||||
|
else:
|
||||||
|
sys.stderr.write('No runway defined in "arrivalspacingdefault"!')
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
# create the sequencing data per runway
|
||||||
|
self.DefaultSequencing = AirportSequencing(icao)
|
||||||
|
for ident in list(filter(None, planning['activearrivalrunwaydefault'].split(':'))):
|
||||||
|
if not ident in spacings:
|
||||||
|
sys.stderr.write('Unable to find sequencing data for ' + ident + ' of ' + icao)
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for runway in self.GngData.Runways[icao]:
|
||||||
|
if ident == runway.Name:
|
||||||
|
sequence = RunwaySequencing(runway)
|
||||||
|
sequence.Spacing = spacings[ident]
|
||||||
|
self.DefaultSequencing.activateRunway(sequence)
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if False == found:
|
||||||
|
sys.stderr.write('Unable to find the runway for ' + ident + ' of ' + icao + ' in SCT data!')
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
# create the dependencies, if needed
|
||||||
|
if True == staggered:
|
||||||
|
if None == planning.get('runwaydependenciesdefault'):
|
||||||
|
sys.stderr.write('Unable to find the runway dependencies for staggered approaches of ' + icao + '!')
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
dependencies = list(filter(None, planning['runwaydependenciesdefault'].split(':')))
|
||||||
|
if 0 != len(dependencies) % 2:
|
||||||
|
sys.stderr.write('No valid set of runway dependencies found!')
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
for i in range(0, len(dependencies), 2):
|
||||||
|
self.DefaultSequencing.addDependency(dependencies[i], dependencies[i + 1])
|
||||||
|
|
||||||
|
def parseConstraints(self, planning):
|
||||||
|
self.ArrivalRouteConstraints = {}
|
||||||
|
|
||||||
|
# check if the IAF sequence constraint is defined
|
||||||
|
if 'iafsequence' in planning:
|
||||||
|
self.IafSpacing = float(planning['iafsequence'])
|
||||||
|
else:
|
||||||
|
self.IafSpacing = 10.0
|
||||||
|
|
||||||
|
# parse the arrival constraints
|
||||||
|
for key in planning:
|
||||||
|
if True == key.startswith('constraints'):
|
||||||
|
star = key.replace('constraints', '').upper()
|
||||||
|
if '' != star:
|
||||||
|
elements = list(filter(None, planning[key].split(':')))
|
||||||
|
if 3 > len(elements):
|
||||||
|
sys.stderr.write('Invalid constraint line: ' + key + '=' + planning[key])
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
waypoints = []
|
||||||
|
|
||||||
|
# values for the waypoint constraints
|
||||||
|
waypointName = elements[0]
|
||||||
|
constraints = [-1, -1]
|
||||||
|
isBaseTurn = False
|
||||||
|
isFinalTurn = False
|
||||||
|
|
||||||
|
index = 1
|
||||||
|
while index < len(elements):
|
||||||
|
if 'A' == elements[index] or 'S' == elements[index]:
|
||||||
|
if index + 1 == len(elements) or False == elements[index + 1].isnumeric():
|
||||||
|
sys.stderr.write('Invalid constraint line: ' + key + '=' + planning[key])
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
if 'A' == elements[index]:
|
||||||
|
constraints[0] = int(elements[index + 1])
|
||||||
|
else:
|
||||||
|
constraints[1] = int(elements[index + 1])
|
||||||
|
index += 1
|
||||||
|
elif 'B' == elements[index]:
|
||||||
|
isBaseTurn = True
|
||||||
|
elif 'F' == elements[index]:
|
||||||
|
isFinalTurn = True
|
||||||
|
else:
|
||||||
|
if False == isBaseTurn and False == isFinalTurn and -1 == constraints[0] and -1 == constraints[1] and '' == waypointName:
|
||||||
|
sys.stderr.write('Invalid constraint line: ' + key + '=' + planning[key])
|
||||||
|
sys.exit(-1)
|
||||||
|
if True == isBaseTurn and True == isFinalTurn:
|
||||||
|
sys.stderr.write('Invalid constraint line: ' + key + '=' + planning[key])
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
waypoints.append(Waypoint(name = waypointName, base = isBaseTurn, final = isFinalTurn))
|
||||||
|
if -1 != constraints[0]:
|
||||||
|
waypoints[-1].Altitude = constraints[0]
|
||||||
|
if -1 != constraints[1]:
|
||||||
|
waypoints[-1].Speed = constraints[1]
|
||||||
|
|
||||||
|
# reset temporary data
|
||||||
|
waypointName = elements[index]
|
||||||
|
constraints = [-1, -1]
|
||||||
|
isBaseTurn = False
|
||||||
|
isFinalTurn = False
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
# check if we have to add the last waypoint
|
||||||
|
if 0 != len(waypoints) and waypointName != waypoints[-1].Name:
|
||||||
|
waypoints.append(Waypoint(name = waypointName, base = isBaseTurn, final = isFinalTurn))
|
||||||
|
if -1 != constraints[0]:
|
||||||
|
waypoints[-1].Altitude = constraints[0]
|
||||||
|
if -1 != constraints[1]:
|
||||||
|
waypoints[-1].Speed = constraints[1]
|
||||||
|
|
||||||
|
# register the arrival route
|
||||||
|
self.ArrivalRouteConstraints[star] = waypoints
|
||||||
|
|
||||||
|
def parseAssignment(assignment : str):
|
||||||
|
elements = list(filter(None, assignment.split(':')))
|
||||||
|
retval = {}
|
||||||
|
type = None
|
||||||
|
|
||||||
|
index = 0
|
||||||
|
while index < len(elements):
|
||||||
|
if 0 == index % 2:
|
||||||
|
if 'A' == elements[index]:
|
||||||
|
type = RunwayAssignmentType.AircraftType
|
||||||
|
elif 'G' == elements[index]:
|
||||||
|
type = RunwayAssignmentType.GateAssignment
|
||||||
|
else:
|
||||||
|
sys.stderr.write('Invalid assignment type: ' + elements[index])
|
||||||
|
sys.exit(-1)
|
||||||
|
else:
|
||||||
|
if None == type:
|
||||||
|
sys.stderr.write('No assignment type defined')
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
if type not in retval:
|
||||||
|
retval.setdefault(type, [])
|
||||||
|
|
||||||
|
retval[type].append(elements[index])
|
||||||
|
type = None
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def findRunway(self, icao : str, name : str):
|
||||||
|
for runway in self.GngData.Runways[icao]:
|
||||||
|
if name == runway.Name:
|
||||||
|
return runway
|
||||||
|
|
||||||
|
sys.stderr.write('Unable to find runway ' + name + ' in the sequencing data for ' + icao)
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
def updateRunwayAssignment(dictionary, runway, assignments):
|
||||||
|
if runway not in dictionary:
|
||||||
|
dictionary.setdefault(runway, {})
|
||||||
|
|
||||||
|
for key in assignments:
|
||||||
|
if key not in dictionary[runway]:
|
||||||
|
dictionary[runway].setdefault(key, assignments[key])
|
||||||
|
else:
|
||||||
|
dictionary[runway][key].extend(assignments[key])
|
||||||
|
|
||||||
|
def parseOptimization(self, key : str, line : str):
|
||||||
|
star = key.replace('optimization', '').upper()
|
||||||
|
|
||||||
|
# check if the STAR exists
|
||||||
|
found = False
|
||||||
|
for rwy in self.GngData.ArrivalRoutes:
|
||||||
|
for route in self.GngData.ArrivalRoutes[rwy]:
|
||||||
|
if star == route.Name:
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if True == found:
|
||||||
|
break
|
||||||
|
|
||||||
|
if False == found:
|
||||||
|
sys.stderr.write('Unknown star:' + key)
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
elements = line.split(':')
|
||||||
|
if 2 != len(elements):
|
||||||
|
sys.stderr.write('Invalid optimization parameter for ' + key)
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
maxTTG = int(elements[0])
|
||||||
|
ttgRatio = float(elements[1])
|
||||||
|
|
||||||
|
return star, maxTTG, ttgRatio
|
||||||
|
|
||||||
|
def updateOptimizationParameters(dictionary, star, maxTTG, ttgRatio):
|
||||||
|
if star not in dictionary:
|
||||||
|
dictionary.setdefault(star, [])
|
||||||
|
dictionary[star] = [ maxTTG, ttgRatio ]
|
||||||
|
|
||||||
|
def parseRunwayAssignment(self, icao : str, planning):
|
||||||
|
self.OptimizationParameters = {}
|
||||||
|
self.RunwayAssignmentsShall = {}
|
||||||
|
self.RunwayAssignmentsShould = {}
|
||||||
|
self.RunwayAssignmentsMay = {}
|
||||||
|
self.MaxDelayMay = timedelta(minutes=10)
|
||||||
|
mayFound = False
|
||||||
|
|
||||||
|
for key in planning:
|
||||||
|
if True == key.startswith('shallassign'):
|
||||||
|
runway = self.findRunway(icao, key.replace('shallassign', '').upper())
|
||||||
|
assignments = Airport.parseAssignment(planning[key])
|
||||||
|
Airport.updateRunwayAssignment(self.RunwayAssignmentsShall, runway, assignments)
|
||||||
|
elif True == key.startswith('shouldassign'):
|
||||||
|
runway = self.findRunway(icao, key.replace('shouldassign', '').upper())
|
||||||
|
assignments = Airport.parseAssignment(planning[key])
|
||||||
|
Airport.updateRunwayAssignment(self.RunwayAssignmentsShould, runway, assignments)
|
||||||
|
elif True == key.startswith('mayassign'):
|
||||||
|
runway = self.findRunway(icao, key.replace('mayassign', '').upper())
|
||||||
|
assignments = Airport.parseAssignment(planning[key])
|
||||||
|
Airport.updateRunwayAssignment(self.RunwayAssignmentsMay, runway, assignments)
|
||||||
|
mayFound = True
|
||||||
|
elif True == key.startswith('optimization'):
|
||||||
|
star, maxTTG, ttgRatio = self.parseOptimization(key, planning[key])
|
||||||
|
Airport.updateOptimizationParameters(self.OptimizationParameters, star, maxTTG, ttgRatio)
|
||||||
|
|
||||||
|
|
||||||
|
# find the max delays
|
||||||
|
if True == mayFound:
|
||||||
|
if 'maxdelaymay' not in planning:
|
||||||
|
sys.stderr.write('maxDelaymay needs to be defined')
|
||||||
|
sys.exit(-1)
|
||||||
|
self.MaxDelayMay = timedelta(minutes=int(planning['maxdelaymay']))
|
||||||
|
|
||||||
|
def parseWebUI(self, webui):
|
||||||
|
self.IafColorization = {}
|
||||||
|
|
||||||
|
for key in webui:
|
||||||
|
if 'iafcolorization' == key:
|
||||||
|
elements = list(filter(None, webui[key].split(':')))
|
||||||
|
for i in range(0, len(elements), 4):
|
||||||
|
self.IafColorization[elements[i]] = [ int(elements[i + 1]), int(elements[i + 2]), int(elements[i + 3]) ]
|
||||||
|
|
||||||
|
def __init__(self, filepath : str, icao : str):
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
config.read(filepath)
|
config.read(filepath)
|
||||||
|
|
||||||
dataConfig = None
|
dataConfig = None
|
||||||
planningConfig = None
|
planningConfig = None
|
||||||
|
rhcConfig = None
|
||||||
|
webUiConfig = None
|
||||||
|
|
||||||
# search the required sections
|
# search the required sections
|
||||||
for key in config:
|
for key in config:
|
||||||
@@ -45,6 +318,10 @@ class Airport:
|
|||||||
dataConfig = config['DATA']
|
dataConfig = config['DATA']
|
||||||
elif 'PLANNING' == key:
|
elif 'PLANNING' == key:
|
||||||
planningConfig = config['PLANNING']
|
planningConfig = config['PLANNING']
|
||||||
|
elif 'RHC' == key:
|
||||||
|
rhcConfig = config['RHC']
|
||||||
|
elif 'WEBUI' == key:
|
||||||
|
webUiConfig = config['WEBUI']
|
||||||
|
|
||||||
# find the GNG-file data
|
# find the GNG-file data
|
||||||
sctFile, eseFile = Airport.findGngData(dataConfig, os.path.dirname(filepath))
|
sctFile, eseFile = Airport.findGngData(dataConfig, os.path.dirname(filepath))
|
||||||
@@ -61,6 +338,40 @@ class Airport:
|
|||||||
sys.stderr.write('No valid planning configuration found')
|
sys.stderr.write('No valid planning configuration found')
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
|
# parse the RHC information
|
||||||
|
if None == rhcConfig:
|
||||||
|
sys.stderr.write('No RHC configuration found')
|
||||||
|
sys.exit(-1)
|
||||||
|
self.RecedingHorizonControl = RHC(rhcConfig)
|
||||||
|
|
||||||
|
# check if thw WebUI information is available
|
||||||
|
if None == webUiConfig:
|
||||||
|
sys.stderr.write('No WEBUI configuration found')
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
# parse the GNG data
|
# parse the GNG data
|
||||||
print('Used GNG-Data: ' + eseFile)
|
print('Used GNG-Data: ' + eseFile)
|
||||||
self.gngData = SctEseFormat(sctFile, eseFile, icao, requiredArrivalRoutes)
|
self.GngData = SctEseFormat(sctFile, eseFile, icao, requiredArrivalRoutes)
|
||||||
|
|
||||||
|
# get the GAFOR id
|
||||||
|
if None == dataConfig.get('gaforid'):
|
||||||
|
sys.stderr.write('No GAFOR-ID found!')
|
||||||
|
sys.exit(-1)
|
||||||
|
self.GaforId = int(dataConfig['gaforid'])
|
||||||
|
|
||||||
|
# get the default sequencing data
|
||||||
|
self.parseDefaultSequencingConfiguration(icao, planningConfig)
|
||||||
|
self.parseConstraints(planningConfig)
|
||||||
|
self.parseRunwayAssignment(icao, planningConfig)
|
||||||
|
self.parseWebUI(webUiConfig)
|
||||||
|
self.assignmentUpdate(self.DefaultSequencing)
|
||||||
|
|
||||||
|
def assignmentUpdate(self, sequenceConfig : AirportSequencing):
|
||||||
|
# initializes the default sequence data
|
||||||
|
for active in sequenceConfig.ActiveArrivalRunways:
|
||||||
|
if active.Runway in self.RunwayAssignmentsShall:
|
||||||
|
active.ShallAssignments = self.RunwayAssignmentsShall[active.Runway]
|
||||||
|
if active.Runway in self.RunwayAssignmentsShould:
|
||||||
|
active.ShouldAssignments = self.RunwayAssignmentsShould[active.Runway]
|
||||||
|
if active.Runway in self.RunwayAssignmentsMay:
|
||||||
|
active.MayAssignments = self.RunwayAssignmentsMay[active.Runway]
|
||||||
|
|||||||
101
aman/config/AirportSequencing.py
Normal file
101
aman/config/AirportSequencing.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from datetime import datetime as dt
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
import time
|
||||||
|
|
||||||
|
from aman.config.RunwaySequencing import RunwaySequencing
|
||||||
|
|
||||||
|
class AirportSequencing:
|
||||||
|
def __init__(self, icao : str):
|
||||||
|
self.Airport = icao
|
||||||
|
self.ActiveArrivalRunways = []
|
||||||
|
self.RunwayDependencies = []
|
||||||
|
self.LastUpdateTimestamp = dt.utcfromtimestamp(int(time.time())).replace(tzinfo = pytz.UTC)
|
||||||
|
self.UseShallShouldMay = True
|
||||||
|
|
||||||
|
def clearData(self):
|
||||||
|
self.ActiveArrivalRunways.clear()
|
||||||
|
self.RunwayDependencies.clear()
|
||||||
|
|
||||||
|
def activateRunway(self, runway : RunwaySequencing):
|
||||||
|
for active in self.ActiveArrivalRunways:
|
||||||
|
if active.Runway.Name == runway.Runway.Name:
|
||||||
|
self.ActiveArrivalRunways[runway.Runway.Name] = runway
|
||||||
|
return
|
||||||
|
self.ActiveArrivalRunways.append(runway)
|
||||||
|
|
||||||
|
def runway(self, index : int):
|
||||||
|
if index >= len(self.ActiveArrivalRunways):
|
||||||
|
return None
|
||||||
|
return self.ActiveArrivalRunways[index].Runway
|
||||||
|
|
||||||
|
def runwayIndex(self, identifier : str):
|
||||||
|
for i in range(0, len(self.ActiveArrivalRunways)):
|
||||||
|
if self.ActiveArrivalRunways[i].Runway.Name == identifier:
|
||||||
|
return i
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def deactivateRunway(self, identifier : str):
|
||||||
|
index = self.runwayIndex(identifier)
|
||||||
|
if 0 <= index:
|
||||||
|
self.ActiveArrivalRunways.pop(index)
|
||||||
|
|
||||||
|
# remove the dependencies
|
||||||
|
for i in range(self.RunwayDependencies - 1, -1, -1):
|
||||||
|
if index == self.RunwayDependencies[i][0] or index == self.RunwayDependencies[i][1]:
|
||||||
|
self.RunwayDependencies.pop(i)
|
||||||
|
|
||||||
|
def addDependency(self, first : str, second : str):
|
||||||
|
idxFirst = self.runwayIndex(first)
|
||||||
|
idxSecond = self.runwayIndex(second)
|
||||||
|
if 0 > idxFirst or 0 > idxSecond:
|
||||||
|
return False
|
||||||
|
|
||||||
|
foundFirst = False
|
||||||
|
foundSecond = False
|
||||||
|
for dependency in self.RunwayDependencies:
|
||||||
|
if idxFirst == dependency[0] and idxSecond == dependency[1]:
|
||||||
|
foundFirst = True
|
||||||
|
elif idxFirst == dependency[1] and idxSecond == dependency[0]:
|
||||||
|
foundSecond = True
|
||||||
|
|
||||||
|
if False == foundFirst:
|
||||||
|
self.RunwayDependencies.append([ idxFirst, idxSecond ])
|
||||||
|
if False == foundSecond:
|
||||||
|
self.RunwayDependencies.append([ idxSecond, idxFirst ])
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def removeDependency(self, first : str, second : str):
|
||||||
|
idxFirst = self.runwayIndex(first)
|
||||||
|
idxSecond = self.runwayIndex(second)
|
||||||
|
if 0 > idxFirst or 0 > idxSecond:
|
||||||
|
return
|
||||||
|
|
||||||
|
for i in range(self.RunwayDependencies - 1, -1, -1):
|
||||||
|
dependency = self.RunwayDependencies[i]
|
||||||
|
|
||||||
|
# check for all the pairs
|
||||||
|
if idxFirst == dependency[0] and idxSecond == dependency[1]:
|
||||||
|
self.RunwayDependencies.pop(i)
|
||||||
|
elif idxSecond == dependency[0] and idxSecond == dependency[0]:
|
||||||
|
self.RunwayDependencies.pop(i)
|
||||||
|
|
||||||
|
def findRunway(self, identifier : str):
|
||||||
|
for runway in self.ActiveArrivalRunways:
|
||||||
|
if runway.Runway.Name == identifier:
|
||||||
|
return runway
|
||||||
|
return None
|
||||||
|
|
||||||
|
def findDependentRunways(self, identifier : str):
|
||||||
|
# runway is unknown
|
||||||
|
index = self.runwayIndex(identifier)
|
||||||
|
if 0 > index:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# search the dependency pair
|
||||||
|
dependencies = [self.ActiveArrivalRunways[self.RunwayDependencies[i][1]] for i in range(0, len(self.RunwayDependencies)) if index == self.RunwayDependencies[i][0]]
|
||||||
|
|
||||||
|
return dependencies
|
||||||
41
aman/config/RHC.py
Normal file
41
aman/config/RHC.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import configparser;
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class RHC():
|
||||||
|
def __init__(self, config : configparser.ConfigParser):
|
||||||
|
# latest scheduling fix in minutes
|
||||||
|
self.FixedBeforeArrival = None
|
||||||
|
# number of seconds per window
|
||||||
|
self.WindowSize = None
|
||||||
|
# number of horizon windows for optimization iteration
|
||||||
|
self.WindowOverlap = None
|
||||||
|
# distance until IAF to add an aircraft to the optimization
|
||||||
|
self.MaximumIafDistance = None
|
||||||
|
|
||||||
|
# search the required sections
|
||||||
|
for key in config:
|
||||||
|
if 'windowsize' == key:
|
||||||
|
self.WindowSize = int(config['windowsize'])
|
||||||
|
elif 'windowoverlap' == key:
|
||||||
|
self.WindowOverlap = int(config['windowoverlap'])
|
||||||
|
elif 'fixedbeforearrival' == key:
|
||||||
|
self.FixedBeforeArrival = timedelta(minutes = int(config['fixedbeforearrival']))
|
||||||
|
elif 'maximumiafdistance' == key:
|
||||||
|
self.MaximumIafDistance = int(config['maximumiafdistance'])
|
||||||
|
|
||||||
|
if self.WindowSize is None:
|
||||||
|
sys.stderr.write('No window size configuration found!')
|
||||||
|
sys.exit(-1)
|
||||||
|
if self.WindowOverlap is None:
|
||||||
|
sys.stderr.write('No window overlap configuration found!')
|
||||||
|
sys.exit(-1)
|
||||||
|
if self.FixedBeforeArrival is None:
|
||||||
|
sys.stderr.write('No fixed before IAF configuration found!')
|
||||||
|
sys.exit(-1)
|
||||||
|
if self.MaximumIafDistance is None:
|
||||||
|
sys.stderr.write('No maximum IAF distance found!')
|
||||||
|
sys.exit(-1)
|
||||||
17
aman/config/RunwaySequencing.py
Normal file
17
aman/config/RunwaySequencing.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from aman.types.Runway import Runway
|
||||||
|
|
||||||
|
class RunwayAssignmentType(Enum):
|
||||||
|
AircraftType = 1
|
||||||
|
GateAssignment = 2
|
||||||
|
|
||||||
|
class RunwaySequencing:
|
||||||
|
def __init__(self, runway : Runway):
|
||||||
|
self.Runway = runway
|
||||||
|
self.Spacing = 3
|
||||||
|
self.ShallAssignments = {}
|
||||||
|
self.ShouldAssignments = {}
|
||||||
|
self.MayAssignments = {}
|
||||||
@@ -8,6 +8,9 @@ class Server():
|
|||||||
self.Address = None
|
self.Address = None
|
||||||
self.PortReceiver = None
|
self.PortReceiver = None
|
||||||
self.PortNotification = None
|
self.PortNotification = None
|
||||||
|
self.WebUiUrl = None
|
||||||
|
self.WebUiSequenceNotification = None
|
||||||
|
self.WebUiConfigurationReceiver = None
|
||||||
|
|
||||||
# search the required sections
|
# search the required sections
|
||||||
for key in config:
|
for key in config:
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import sys
|
|||||||
class Weather():
|
class Weather():
|
||||||
def __init__(self, config : configparser.ConfigParser):
|
def __init__(self, config : configparser.ConfigParser):
|
||||||
self.Provider = None
|
self.Provider = None
|
||||||
self.PortReceiver = None
|
|
||||||
self.PortNotification = None
|
|
||||||
|
|
||||||
# search the required sections
|
# search the required sections
|
||||||
for key in config:
|
for key in config:
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import copy
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from aman.types.ArrivalRoute import ArrivalRoute
|
from aman.types.ArrivalRoute import ArrivalRoute
|
||||||
|
from aman.types.Runway import Runway
|
||||||
from aman.types.Waypoint import Waypoint
|
from aman.types.Waypoint import Waypoint
|
||||||
|
|
||||||
class SctEseFormat:
|
class SctEseFormat:
|
||||||
@@ -30,11 +32,22 @@ class SctEseFormat:
|
|||||||
if len(split) <= longitudeIdx:
|
if len(split) <= longitudeIdx:
|
||||||
sys.stderr.write('Invalid waypoint format: ' + waypoint)
|
sys.stderr.write('Invalid waypoint format: ' + waypoint)
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
return Waypoint(split[nameIdx], Waypoint.dms2dd(split[latitudeIdx]), Waypoint.dms2dd(split[longitudeIdx]))
|
return Waypoint(name = split[nameIdx], latitude = split[latitudeIdx], longitude = split[longitudeIdx])
|
||||||
|
|
||||||
def extractWaypoints(self, sctFilepath : str):
|
def parseRunway(runway : str):
|
||||||
|
split = list(filter(None, runway.split(' ')))
|
||||||
|
if 9 != len(split) or '' == split[8]:
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
waypoint0 = Waypoint(name = split[0], latitude = split[4], longitude = split[5])
|
||||||
|
waypoint1 = Waypoint(name = split[1], latitude = split[6], longitude = split[7])
|
||||||
|
|
||||||
|
return split[8], Runway(waypoint0, waypoint1), Runway(waypoint1, waypoint0)
|
||||||
|
|
||||||
|
def extractSctInformation(self, sctFilepath : str):
|
||||||
config = SctEseFormat.readFile(sctFilepath)
|
config = SctEseFormat.readFile(sctFilepath)
|
||||||
foundAirports = False
|
foundAirports = False
|
||||||
|
foundRunways = False
|
||||||
foundVOR = False
|
foundVOR = False
|
||||||
foundNDB = False
|
foundNDB = False
|
||||||
foundFix = False
|
foundFix = False
|
||||||
@@ -48,6 +61,8 @@ class SctEseFormat:
|
|||||||
foundFix = True
|
foundFix = True
|
||||||
elif 'AIRPORT' == key:
|
elif 'AIRPORT' == key:
|
||||||
foundAirports = True
|
foundAirports = True
|
||||||
|
elif 'RUNWAY' == key:
|
||||||
|
foundRunways = True
|
||||||
|
|
||||||
if False == foundVOR:
|
if False == foundVOR:
|
||||||
sys.stderr.write('Unable to find VOR-entries in the sector file')
|
sys.stderr.write('Unable to find VOR-entries in the sector file')
|
||||||
@@ -61,27 +76,39 @@ class SctEseFormat:
|
|||||||
if False == foundAirports:
|
if False == foundAirports:
|
||||||
sys.stderr.write('Unable to find AIRPORT-entries in the sector file')
|
sys.stderr.write('Unable to find AIRPORT-entries in the sector file')
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
if False == foundRunways:
|
||||||
|
sys.stderr.write('Unable to find RUNWAY-entries in the sector file')
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
# extract all waypoints
|
# extract all waypoints
|
||||||
for waypoint in config['VOR']:
|
for waypoint in config['VOR']:
|
||||||
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 2, 3)
|
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 2, 3)
|
||||||
self.waypoints.setdefault(waypoint.name, []).append(waypoint)
|
self.Waypoints.setdefault(waypoint.Name, []).append(waypoint)
|
||||||
for waypoint in config['NDB']:
|
for waypoint in config['NDB']:
|
||||||
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 1, 2)
|
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 1, 2)
|
||||||
self.waypoints.setdefault(waypoint.name, []).append(waypoint)
|
self.Waypoints.setdefault(waypoint.Name, []).append(waypoint)
|
||||||
for waypoint in config['FIXES']:
|
for waypoint in config['FIXES']:
|
||||||
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 1, 2)
|
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 1, 2)
|
||||||
self.waypoints.setdefault(waypoint.name, []).append(waypoint)
|
self.Waypoints.setdefault(waypoint.Name, []).append(waypoint)
|
||||||
|
|
||||||
# extract the airports
|
# extract the airports
|
||||||
for airport in config['AIRPORT']:
|
for airport in config['AIRPORT']:
|
||||||
airport = SctEseFormat.parseWaypoint(airport,0, 2, 3)
|
airport = SctEseFormat.parseWaypoint(airport, 0, 2, 3)
|
||||||
self.airports.setdefault(airport.name, []).append(airport)
|
self.Airports.setdefault(airport.Name, []).append(airport)
|
||||||
|
|
||||||
|
# extract the runways
|
||||||
|
for runway in config['RUNWAY']:
|
||||||
|
airport, runway0, runway1 = SctEseFormat.parseRunway(runway)
|
||||||
|
if None != airport:
|
||||||
|
if not airport in self.Runways:
|
||||||
|
self.Runways.setdefault(airport, [])
|
||||||
|
self.Runways[airport].append(runway0)
|
||||||
|
self.Runways[airport].append(runway1)
|
||||||
|
|
||||||
def parseArrivalRoute(self, route : str, airport : Waypoint):
|
def parseArrivalRoute(self, route : str, airport : Waypoint):
|
||||||
# split the route and validate that it is a STAR for the airport
|
# split the route and validate that it is a STAR for the airport
|
||||||
split = route.split(':')
|
split = route.split(':')
|
||||||
if 5 != len(split) or 'STAR' != split[0] or split[1] != airport.name:
|
if 5 != len(split) or 'STAR' != split[0] or split[1] != airport.Name:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# find all waypoints
|
# find all waypoints
|
||||||
@@ -89,7 +116,7 @@ class SctEseFormat:
|
|||||||
route = list(filter(None, split[4].split(' ')))
|
route = list(filter(None, split[4].split(' ')))
|
||||||
for waypoint in route:
|
for waypoint in route:
|
||||||
# find the waypoint in the route
|
# find the waypoint in the route
|
||||||
coordinates = self.waypoints[waypoint]
|
coordinates = self.Waypoints[waypoint]
|
||||||
# no waypoint with this name defined
|
# no waypoint with this name defined
|
||||||
if None == coordinates:
|
if None == coordinates:
|
||||||
sys.stderr.write('Unable to find waypoint ' + waypoint)
|
sys.stderr.write('Unable to find waypoint ' + waypoint)
|
||||||
@@ -111,10 +138,10 @@ class SctEseFormat:
|
|||||||
sys.stderr.write('Unable to find a close waypoint for ' + waypoint)
|
sys.stderr.write('Unable to find a close waypoint for ' + waypoint)
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
waypoints.append(nearest)
|
waypoints.append(copy.deepcopy(nearest))
|
||||||
# extend the list of waypoints
|
# extend the list of waypoints
|
||||||
else:
|
else:
|
||||||
waypoints.append(coordinates[0])
|
waypoints.append(copy.deepcopy(coordinates[0]))
|
||||||
|
|
||||||
# create the arrival route
|
# create the arrival route
|
||||||
return ArrivalRoute(split[3], split[2], waypoints)
|
return ArrivalRoute(split[3], split[2], waypoints)
|
||||||
@@ -124,10 +151,10 @@ class SctEseFormat:
|
|||||||
foundSidsStars = False
|
foundSidsStars = False
|
||||||
|
|
||||||
# search the airport in the extracted list
|
# search the airport in the extracted list
|
||||||
if not airport in self.airports:
|
if not airport in self.Airports:
|
||||||
sys.stderr.write(airport + 'in self.airports', 'Unable to find the requested airport')
|
sys.stderr.write('Unable to find the requested airport')
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
airport = self.airports[airport][0]
|
airport = self.Airports[airport][0]
|
||||||
|
|
||||||
for key in config:
|
for key in config:
|
||||||
if 'SIDSSTARS' == key:
|
if 'SIDSSTARS' == key:
|
||||||
@@ -140,13 +167,14 @@ class SctEseFormat:
|
|||||||
# parse all arrival routes
|
# parse all arrival routes
|
||||||
for line in config['SIDSSTARS']:
|
for line in config['SIDSSTARS']:
|
||||||
route = self.parseArrivalRoute(line, airport)
|
route = self.parseArrivalRoute(line, airport)
|
||||||
if None != route and route.name in allowedRoutes:
|
if None != route and route.Name in allowedRoutes:
|
||||||
self.arrivalRoutes.setdefault(route.runway, []).append(route)
|
self.ArrivalRoutes.setdefault(route.Runway, []).append(route)
|
||||||
|
|
||||||
def __init__(self, sctFilepath : str, eseFilepath : str, airport : str, allowedRoutes : list):
|
def __init__(self, sctFilepath : str, eseFilepath : str, airport : str, allowedRoutes : list):
|
||||||
self.arrivalRoutes = {}
|
self.ArrivalRoutes = {}
|
||||||
self.waypoints = {}
|
self.Waypoints = {}
|
||||||
self.airports = {}
|
self.Airports = {}
|
||||||
|
self.Runways = {}
|
||||||
|
|
||||||
self.extractWaypoints(sctFilepath)
|
self.extractSctInformation(sctFilepath)
|
||||||
self.extractArrivalRoutes(eseFilepath, airport, allowedRoutes)
|
self.extractArrivalRoutes(eseFilepath, airport, allowedRoutes)
|
||||||
|
|||||||
256
aman/sys/RecedingHorizonControl.py
Normal file
256
aman/sys/RecedingHorizonControl.py
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
#!/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):
|
||||||
|
if 0 == len(self.Windows):
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
inbounds = []
|
||||||
|
if self.FreezedIndex + 1 >= len(self.Windows):
|
||||||
|
earliestArrivalTime = dt.utcfromtimestamp(int(time.time())).replace(tzinfo = pytz.UTC)
|
||||||
|
earliestArrivalTime += self.Configuration.FixedBeforeArrival
|
||||||
|
else:
|
||||||
|
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, min(len(self.Windows), 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)
|
||||||
33
aman/sys/RecedingHorizonWindow.py
Normal file
33
aman/sys/RecedingHorizonWindow.py
Normal file
@@ -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.Callsign == inbound.Callsign:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def inbound(self, callsign : str):
|
||||||
|
for report in self.Inbounds:
|
||||||
|
if report.Callsign == callsign:
|
||||||
|
return report
|
||||||
|
return None
|
||||||
|
|
||||||
|
def insert(self, inbound : Inbound):
|
||||||
|
for i in range(0, len(self.Inbounds)):
|
||||||
|
if self.Inbounds[i].Callsign == inbound.Callsign:
|
||||||
|
return
|
||||||
|
self.Inbounds.append(inbound)
|
||||||
|
|
||||||
|
def remove(self, callsign : str):
|
||||||
|
for i in range(0, len(self.Inbounds)):
|
||||||
|
if self.Inbounds[i].Callsign == callsign:
|
||||||
|
self.Inbounds.pop(i)
|
||||||
|
return
|
||||||
@@ -7,13 +7,16 @@ import scipy.interpolate
|
|||||||
|
|
||||||
class WeatherModel:
|
class WeatherModel:
|
||||||
def __init__(self, gaforId, weather : Weather):
|
def __init__(self, gaforId, weather : Weather):
|
||||||
self.gafor = gaforId
|
self.Gafor = gaforId
|
||||||
self.weather = weather
|
self.Weather = weather
|
||||||
self.windDirectionModel = None
|
self.Altitudes = None
|
||||||
self.windSpeedModel = None
|
self.Directions = None
|
||||||
self.lastWeatherUpdate = None
|
self.Windspeeds = None
|
||||||
self.minimumAltitude = 1000000
|
self.WindDirectionModel = None
|
||||||
self.maximumAltitude = -1
|
self.WindSpeedModel = None
|
||||||
|
self.LastWeatherUpdate = None
|
||||||
|
self.MinimumAltitude = 1000000
|
||||||
|
self.MaximumAltitude = -1
|
||||||
|
|
||||||
# create the density interpolation model
|
# create the density interpolation model
|
||||||
# the density model is based on https://aerotoolbox.com/atmcalc/
|
# the density model is based on https://aerotoolbox.com/atmcalc/
|
||||||
@@ -95,54 +98,87 @@ class WeatherModel:
|
|||||||
return ias * math.sqrt(1.225 / self.densityModel(altitude).item())
|
return ias * math.sqrt(1.225 / self.densityModel(altitude).item())
|
||||||
|
|
||||||
def updateWindModel(self):
|
def updateWindModel(self):
|
||||||
if None == self.lastWeatherUpdate or self.lastWeatherUpdate != self.weather.provider.updateTime:
|
if None == self.Weather or None == self.Weather.Provider:
|
||||||
self.lastWeatherUpdate = self.weather.provider.updateTime
|
return
|
||||||
|
|
||||||
self.minimumAltitude = 1000000
|
if None == self.LastWeatherUpdate or self.LastWeatherUpdate != self.Weather.Provider.UpdateTime:
|
||||||
self.maximumAltitude = -1
|
self.MinimumAltitude = 1000000
|
||||||
self.windDirectionModel = None
|
self.MaximumAltitude = -1
|
||||||
self.windSpeedModel = None
|
self.WindDirectionModel = None
|
||||||
|
self.WindSpeedModel = None
|
||||||
|
self.Altitudes = None
|
||||||
|
self.Directions = None
|
||||||
|
self.Windspeeds = None
|
||||||
|
|
||||||
if None != self.weather.provider.windData and self.gafor in self.weather.provider.windData:
|
if None != self.Weather.Provider.WindData and self.Gafor in self.Weather.Provider.WindData:
|
||||||
altitudes = []
|
self.Altitudes = []
|
||||||
directions = []
|
self.Directions = []
|
||||||
speeds = []
|
self.Windspeeds = []
|
||||||
|
|
||||||
# collect the data for the wind model
|
# collect the data for the wind model
|
||||||
for level in self.weather.provider.windData[self.gafor]:
|
for level in self.Weather.Provider.WindData[self.Gafor]:
|
||||||
altitudes.append(level[0])
|
self.Altitudes.append(level[0])
|
||||||
directions.append(level[1])
|
self.Directions.append(level[1])
|
||||||
speeds.append(level[2])
|
self.Windspeeds.append(level[2])
|
||||||
|
|
||||||
# define the thresholds for later boundary checks
|
# define the thresholds for later boundary checks
|
||||||
if self.minimumAltitude > level[0]:
|
if self.MinimumAltitude > level[0]:
|
||||||
self.minimumAltitude = level[0]
|
self.MinimumAltitude = level[0]
|
||||||
if self.maximumAltitude < level[0]:
|
if self.MaximumAltitude < level[0]:
|
||||||
self.maximumAltitude = level[0]
|
self.MaximumAltitude = level[0]
|
||||||
|
|
||||||
# calculate the models
|
# calculate the models
|
||||||
if 1 < len(altitudes):
|
if 1 < len(self.Altitudes):
|
||||||
self.windDirectionModel = scipy.interpolate.interp1d(altitudes, directions)
|
self.WindDirectionModel = scipy.interpolate.interp1d(self.Altitudes, self.Directions)
|
||||||
self.windSpeedModel = scipy.interpolate.interp1d(altitudes, speeds)
|
self.WindSpeedModel = scipy.interpolate.interp1d(self.Altitudes, self.Windspeeds)
|
||||||
|
self.LastWeatherUpdate = self.Weather.Provider.UpdateTime
|
||||||
|
else:
|
||||||
|
self.LastWeatherUpdate = None
|
||||||
|
|
||||||
def calculateGS(self, altitude : int, ias : int, heading : int):
|
def interpolateWindData(self, altitude : int):
|
||||||
self.updateWindModel()
|
self.updateWindModel()
|
||||||
tas = self.calculateTAS(altitude, ias)
|
|
||||||
|
|
||||||
# initialize the wind data
|
# initialized the wind data
|
||||||
if None != self.windDirectionModel and None != self.windSpeedModel:
|
if None != self.WindDirectionModel and None != self.WindSpeedModel:
|
||||||
direction = 0.0
|
direction = 0.0
|
||||||
speed = 0.0
|
speed = 0.0
|
||||||
if None != self.windSpeedModel and None != self.windDirectionModel:
|
if None != self.WindSpeedModel and None != self.WindDirectionModel:
|
||||||
if self.maximumAltitude <= altitude:
|
if self.MaximumAltitude <= altitude:
|
||||||
altitude = self.maximumAltitude - 1
|
altitude = self.MaximumAltitude - 1
|
||||||
if self.minimumAltitude >= altitude:
|
if self.MinimumAltitude >= altitude:
|
||||||
altitude = self.minimumAltitude + 1
|
altitude = self.MinimumAltitude + 1
|
||||||
direction = self.windDirectionModel(altitude).item()
|
direction = self.WindDirectionModel(altitude).item()
|
||||||
speed = self.windSpeedModel(altitude).item()
|
speed = self.WindSpeedModel(altitude).item()
|
||||||
else:
|
else:
|
||||||
speed = 0
|
speed = 0
|
||||||
direction = 0
|
direction = 0
|
||||||
|
|
||||||
# calculate the ground speed based on the headwind component
|
return speed, direction
|
||||||
|
|
||||||
|
def calculateGS(self, altitude : int, ias : int, heading : int):
|
||||||
|
speed, direction = self.interpolateWindData(altitude)
|
||||||
|
tas = self.calculateTAS(altitude, ias)
|
||||||
return tas + speed * math.cos(math.radians(direction) - math.radians(heading))
|
return tas + speed * math.cos(math.radians(direction) - math.radians(heading))
|
||||||
|
|
||||||
|
def convertGSToTAS(self, altitude : int, gs : int, heading : int):
|
||||||
|
speed, direction = self.interpolateWindData(altitude)
|
||||||
|
return gs - speed * math.cos(math.radians(direction) - math.radians(heading))
|
||||||
|
|
||||||
|
def estimateCourse(self, altitude : int, gs : int, heading : int):
|
||||||
|
tas = self.convertGSToTAS(altitude, gs, heading)
|
||||||
|
speed, direction = self.interpolateWindData(altitude)
|
||||||
|
|
||||||
|
aca = heading - direction
|
||||||
|
wca = speed * aca / tas
|
||||||
|
|
||||||
|
if 0 <= aca:
|
||||||
|
course = heading + wca
|
||||||
|
else:
|
||||||
|
course = heading - wca
|
||||||
|
|
||||||
|
while 0 > course:
|
||||||
|
course += 360
|
||||||
|
while 360 < course:
|
||||||
|
course -= 360
|
||||||
|
|
||||||
|
return course
|
||||||
|
|||||||
@@ -1,53 +1,140 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
from threading import Thread, Lock
|
from threading import Thread, Lock
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from aman.com import Weather
|
||||||
|
from aman.com.Euroscope import Euroscope
|
||||||
from aman.config.Airport import Airport
|
from aman.config.Airport import Airport
|
||||||
|
from aman.config.AirportSequencing import AirportSequencing
|
||||||
|
from aman.sys.aco.Colony import Colony
|
||||||
|
from aman.sys.aco.Configuration import Configuration
|
||||||
|
from aman.sys.aco.Node import Node
|
||||||
|
from aman.sys.WeatherModel import WeatherModel
|
||||||
|
from aman.sys.RecedingHorizonControl import RecedingHorizonControl
|
||||||
|
from aman.types.Inbound import Inbound
|
||||||
|
from aman.types.PerformanceData import PerformanceData
|
||||||
|
|
||||||
class Worker(Thread):
|
class Worker(Thread):
|
||||||
def __init__(self):
|
def __init__(self, icao : str, configuration : Airport, weather : Weather,
|
||||||
|
performance : PerformanceData, euroscope : Euroscope):
|
||||||
Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
self.stopThread = None
|
self.StopThread = None
|
||||||
self.icao = None
|
self.Icao = icao
|
||||||
self.configuration = None
|
self.Configuration = configuration
|
||||||
self.arrivalRoutes = None
|
self.SequencingConfiguration = configuration.DefaultSequencing
|
||||||
self.updateLock = None
|
self.PerformanceData = performance
|
||||||
self.reportQueue = {}
|
self.UpdateLock = Lock()
|
||||||
|
self.ReportQueue = {}
|
||||||
|
if None != weather:
|
||||||
|
self.WeatherModel = WeatherModel(configuration.GaforId, weather)
|
||||||
|
else:
|
||||||
|
self.WeatherModel = WeatherModel(0, None)
|
||||||
|
self.RecedingHorizonControl = RecedingHorizonControl(configuration.RecedingHorizonControl)
|
||||||
|
self.Euroscope = euroscope
|
||||||
|
|
||||||
def __del__(self):
|
# merge the constraint information with the GNG information
|
||||||
self.release()
|
for runway in self.Configuration.GngData.ArrivalRoutes:
|
||||||
|
for star in self.Configuration.GngData.ArrivalRoutes[runway]:
|
||||||
|
for name in self.Configuration.ArrivalRouteConstraints:
|
||||||
|
if name == star.Name:
|
||||||
|
for constraint in self.Configuration.ArrivalRouteConstraints[name]:
|
||||||
|
foundWaypoint = False
|
||||||
|
|
||||||
def acquire(self, icao : str, configuration : Airport):
|
for waypoint in star.Route:
|
||||||
self.stopThread = None
|
if constraint.Name == waypoint.Name:
|
||||||
self.icao = icao
|
waypoint.Altitude = constraint.Altitude
|
||||||
self.configuration = configuration
|
waypoint.Speed = constraint.Speed
|
||||||
self.arrivalRoutes = configuration.gngData.arrivalRoutes
|
waypoint.BaseTurn = constraint.BaseTurn
|
||||||
self.updateLock = Lock()
|
waypoint.FinalTurn = constraint.FinalTurn
|
||||||
self.reportQueue = {}
|
foundWaypoint = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if False == foundWaypoint:
|
||||||
|
sys.stderr.write('Unable to find ' + constraint.Name + ' in ' + name)
|
||||||
|
sys.exit(-1)
|
||||||
|
break
|
||||||
|
|
||||||
|
self.setDaemon(True)
|
||||||
self.start()
|
self.start()
|
||||||
|
|
||||||
def acquireLock(self):
|
def acquireLock(self):
|
||||||
if None != self.updateLock:
|
if None != self.UpdateLock:
|
||||||
self.updateLock.acquire()
|
self.UpdateLock.acquire()
|
||||||
|
|
||||||
def release(self):
|
|
||||||
self.stopThread = True
|
|
||||||
self.join()
|
|
||||||
|
|
||||||
def releaseLock(self):
|
def releaseLock(self):
|
||||||
if None != self.updateLock:
|
if None != self.UpdateLock:
|
||||||
self.updateLock.release()
|
self.UpdateLock.release()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
counter = 0
|
counter = 0
|
||||||
|
|
||||||
while None == self.stopThread:
|
while None == self.StopThread:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
counter += 1
|
counter += 1
|
||||||
if 0 != (counter % 60):
|
if 0 != (counter % 60):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# TODO handle the report queue and update internal information
|
self.acquireLock()
|
||||||
# TODO execute planning, etc.
|
|
||||||
continue
|
# perform some book-keeping
|
||||||
|
self.RecedingHorizonControl.cleanupWindows()
|
||||||
|
|
||||||
|
# update the aircraft information in RHC
|
||||||
|
for callsign in self.ReportQueue:
|
||||||
|
report = self.ReportQueue[callsign]
|
||||||
|
|
||||||
|
if '' != report.initialApproachFix:
|
||||||
|
inbound = Inbound(report, self.PerformanceData)
|
||||||
|
Node(inbound, inbound.ReportTime, self.WeatherModel, self.Configuration, self.SequencingConfiguration)
|
||||||
|
if None != inbound.EnrouteArrivalTime:
|
||||||
|
self.RecedingHorizonControl.updateReport(inbound)
|
||||||
|
else:
|
||||||
|
print('Unable to find all data of ' + report.aircraft.callsign)
|
||||||
|
|
||||||
|
self.ReportQueue.clear()
|
||||||
|
|
||||||
|
# search the ACO relevant aircrafts
|
||||||
|
relevantInbounds, earliestArrivalTime = self.RecedingHorizonControl.optimizationRelevantInbounds()
|
||||||
|
if None != relevantInbounds:
|
||||||
|
start = time.process_time()
|
||||||
|
|
||||||
|
# get the last landing aircrafts per runway before the RHC stage to check for constraints
|
||||||
|
# this is required to handle the overlap between windows
|
||||||
|
runways, iafs = self.RecedingHorizonControl.latestFixedInbounds(self.Configuration, self.SequencingConfiguration)
|
||||||
|
|
||||||
|
# configure the ACO run
|
||||||
|
acoConfig = Configuration(constraints = self.SequencingConfiguration, config = self.Configuration,
|
||||||
|
earliest = earliestArrivalTime, weather = self.WeatherModel,
|
||||||
|
preceedingRunways = runways, preceedingIafs = iafs,
|
||||||
|
ants = 5 * len(relevantInbounds), generations = 5 * len(relevantInbounds))
|
||||||
|
|
||||||
|
# run the optimizer outside the locking functions
|
||||||
|
self.releaseLock()
|
||||||
|
# perform the ACO run
|
||||||
|
aco = Colony(relevantInbounds, acoConfig)
|
||||||
|
aco.optimize()
|
||||||
|
self.acquireLock()
|
||||||
|
|
||||||
|
if None != aco.Result:
|
||||||
|
for node in aco.Result:
|
||||||
|
self.RecedingHorizonControl.resequenceInbound(node.Inbound)
|
||||||
|
|
||||||
|
# measure the exuction time of the overall optimization process
|
||||||
|
executionTime = time.process_time() - start
|
||||||
|
if 60.0 <= executionTime:
|
||||||
|
print('Optimized ' + str(len(aco.Result)) + ' inbounds in ' + str(executionTime) + ' seconds')
|
||||||
|
|
||||||
|
self.releaseLock()
|
||||||
|
|
||||||
|
def inboundSequence(self):
|
||||||
|
self.acquireLock()
|
||||||
|
sequence = self.RecedingHorizonControl.sequence()
|
||||||
|
self.releaseLock()
|
||||||
|
return sequence
|
||||||
|
|
||||||
|
def configure(self, configuration : AirportSequencing):
|
||||||
|
self.acquireLock()
|
||||||
|
self.SequencingConfiguration = configuration
|
||||||
|
self.releaseLock()
|
||||||
|
|||||||
122
aman/sys/aco/Ant.py
Normal file
122
aman/sys/aco/Ant.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import math
|
||||||
|
import numpy as np
|
||||||
|
import random
|
||||||
|
import bisect
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
from aman.sys.aco.Configuration import Configuration
|
||||||
|
from aman.sys.aco.RunwayManager import RunwayManager
|
||||||
|
from aman.sys.aco.Node import Node
|
||||||
|
|
||||||
|
# This class implements a single ant of the following paper:
|
||||||
|
# https://sci-hub.mksa.top/10.1109/cec.2019.8790135
|
||||||
|
class Ant:
|
||||||
|
def __init__(self, pheromoneTable : np.array, configuration : Configuration, nodes):
|
||||||
|
self.Configuration = configuration
|
||||||
|
self.Nodes = nodes
|
||||||
|
self.RunwayManager = RunwayManager(self.Configuration)
|
||||||
|
self.InboundSelected = [ False ] * len(self.Nodes)
|
||||||
|
self.InboundScore = np.zeros([ len(self.Nodes), 1 ])
|
||||||
|
self.PheromoneMatrix = pheromoneTable
|
||||||
|
self.SequenceDelay = timedelta(seconds = 0)
|
||||||
|
self.Sequence = None
|
||||||
|
|
||||||
|
# Implements function (5)
|
||||||
|
def heuristicInformation(self, current : int):
|
||||||
|
_, _, _, eta, _ = self.RunwayManager.selectArrivalRunway(self.Nodes[current], self.Configuration.EarliestArrivalTime)
|
||||||
|
if None == eta:
|
||||||
|
return -1.0
|
||||||
|
|
||||||
|
inboundDelay = eta - self.Nodes[current].Inbound.InitialArrivalTime
|
||||||
|
|
||||||
|
# calculate the fraction with a mix of the unused runway time and the delay of single aircrafts
|
||||||
|
heuristic = inboundDelay.total_seconds() / 60.0
|
||||||
|
heuristic = (1.0 / (heuristic or 1)) ** self.Configuration.Beta
|
||||||
|
return heuristic
|
||||||
|
|
||||||
|
# Implements functions (3), (6)
|
||||||
|
def selectNextLandingIndex(self):
|
||||||
|
q = float(random.randint(0, 100)) / 100
|
||||||
|
weights = []
|
||||||
|
|
||||||
|
if q <= self.Configuration.PseudoRandomSelectionRate:
|
||||||
|
for i in range(0, len(self.InboundSelected)):
|
||||||
|
if False == self.InboundSelected[i]:
|
||||||
|
weights.append(self.heuristicInformation(i))
|
||||||
|
else:
|
||||||
|
# roulette selection strategy
|
||||||
|
pheromoneScale = 0.0
|
||||||
|
for i in range(0, len(self.InboundSelected)):
|
||||||
|
if False == self.InboundSelected[i]:
|
||||||
|
pheromoneScale += self.heuristicInformation(i)
|
||||||
|
|
||||||
|
for i in range(0, len(self.InboundSelected)):
|
||||||
|
if False == self.InboundSelected[i]:
|
||||||
|
weights.append(self.heuristicInformation(i) / (pheromoneScale or 1))
|
||||||
|
|
||||||
|
# something was wrong in the runway selection
|
||||||
|
if -1.0 in weights:
|
||||||
|
return None
|
||||||
|
|
||||||
|
total = sum(weights)
|
||||||
|
cumdist = list(itertools.accumulate(weights)) + [total]
|
||||||
|
candidateIndex = bisect.bisect(cumdist, random.random() * total)
|
||||||
|
|
||||||
|
for i in range(0, len(self.InboundSelected)):
|
||||||
|
if False == self.InboundSelected[i]:
|
||||||
|
if 0 == candidateIndex:
|
||||||
|
return i
|
||||||
|
else:
|
||||||
|
candidateIndex -= 1
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def associateInbound(self, node : Node, earliestArrivalTime : datetime):
|
||||||
|
# prepare the statistics
|
||||||
|
_, _, rwy, eta, _ = self.RunwayManager.selectArrivalRunway(node, self.Configuration.EarliestArrivalTime)
|
||||||
|
eta = max(earliestArrivalTime, eta)
|
||||||
|
|
||||||
|
node.Inbound.PlannedRunway = rwy
|
||||||
|
node.Inbound.PlannedStar = node.ArrivalCandidates[rwy.Name].Star
|
||||||
|
node.Inbound.PlannedArrivalTime = eta
|
||||||
|
node.Inbound.PlannedArrivalRoute = node.ArrivalCandidates[rwy.Name].ArrivalRoute
|
||||||
|
node.Inbound.InitialArrivalTime = node.ArrivalCandidates[rwy.Name].InitialArrivalTime
|
||||||
|
self.RunwayManager.registerNode(node, rwy.Name)
|
||||||
|
|
||||||
|
delay = node.Inbound.PlannedArrivalTime - node.Inbound.InitialArrivalTime
|
||||||
|
if 0.0 < delay.total_seconds():
|
||||||
|
return delay, rwy
|
||||||
|
else:
|
||||||
|
return timedelta(seconds = 0), rwy
|
||||||
|
|
||||||
|
def findSolution(self, first : int):
|
||||||
|
self.Sequence = []
|
||||||
|
|
||||||
|
# select the first inbound
|
||||||
|
self.InboundSelected[first] = True
|
||||||
|
delay, _ = self.associateInbound(self.Nodes[first], self.Configuration.EarliestArrivalTime)
|
||||||
|
self.Sequence.append(first)
|
||||||
|
self.SequenceDelay += delay
|
||||||
|
|
||||||
|
while 1:
|
||||||
|
index = self.selectNextLandingIndex()
|
||||||
|
if None == index:
|
||||||
|
break
|
||||||
|
|
||||||
|
self.InboundSelected[index] = True
|
||||||
|
delay, _ = self.associateInbound(self.Nodes[index], self.Configuration.EarliestArrivalTime)
|
||||||
|
self.SequenceDelay += delay
|
||||||
|
self.Sequence.append(index)
|
||||||
|
|
||||||
|
# update the local pheromone
|
||||||
|
update = (1.0 - self.Configuration.PropagationRatio) * self.PheromoneMatrix[self.Sequence[-2], self.Sequence[-1]]
|
||||||
|
update += self.Configuration.PropagationRatio * self.Configuration.ThetaZero
|
||||||
|
self.PheromoneMatrix[self.Sequence[-2], self.Sequence[-1]] = max(self.Configuration.ThetaZero, update)
|
||||||
|
|
||||||
|
# validate that nothing went wrong
|
||||||
|
if len(self.Sequence) != len(self.Nodes):
|
||||||
|
self.SequenceDelay = None
|
||||||
|
self.Sequence = None
|
||||||
155
aman/sys/aco/Colony.py
Normal file
155
aman/sys/aco/Colony.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from datetime import datetime as dt
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import numpy as np
|
||||||
|
import pytz
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from aman.sys.aco.Ant import Ant
|
||||||
|
from aman.sys.aco.Configuration import Configuration
|
||||||
|
from aman.sys.aco.Node import Node
|
||||||
|
from aman.sys.aco.RunwayManager import RunwayManager
|
||||||
|
from aman.types.Inbound import Inbound
|
||||||
|
|
||||||
|
# This class implements the ant colony of the following paper:
|
||||||
|
# https://sci-hub.mksa.top/10.1109/cec.2019.8790135
|
||||||
|
class Colony:
|
||||||
|
def associateInbound(rwyManager : RunwayManager, node : Node, earliestArrivalTime : datetime):
|
||||||
|
type, expectedRwy, rwy, eta, _ = rwyManager.selectArrivalRunway(node, earliestArrivalTime)
|
||||||
|
if None == eta:
|
||||||
|
return False
|
||||||
|
eta = max(earliestArrivalTime, eta)
|
||||||
|
|
||||||
|
node.Inbound.PlannedRunway = rwy
|
||||||
|
node.Inbound.PlannedStar = node.ArrivalCandidates[rwy.Name].Star
|
||||||
|
node.Inbound.PlannedArrivalRoute = node.ArrivalCandidates[rwy.Name].ArrivalRoute
|
||||||
|
node.Inbound.PlannedArrivalTime = eta
|
||||||
|
node.Inbound.InitialArrivalTime = node.ArrivalCandidates[rwy.Name].InitialArrivalTime
|
||||||
|
node.Inbound.PlannedTrackmiles = node.ArrivalCandidates[rwy.Name].Trackmiles
|
||||||
|
node.Inbound.AssignmentMode = type
|
||||||
|
node.Inbound.ExpectedRunway = expectedRwy
|
||||||
|
rwyManager.registerNode(node, rwy.Name)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def calculateInitialCosts(rwyManager : RunwayManager, nodes, earliestArrivalTime : datetime):
|
||||||
|
overallDelay = timedelta(seconds = 0)
|
||||||
|
|
||||||
|
# assume that the nodes are sorted in FCFS order
|
||||||
|
for node in nodes:
|
||||||
|
if False == Colony.associateInbound(rwyManager, node, earliestArrivalTime):
|
||||||
|
return None
|
||||||
|
overallDelay += node.Inbound.PlannedArrivalTime - node.Inbound.InitialArrivalTime
|
||||||
|
|
||||||
|
return overallDelay
|
||||||
|
|
||||||
|
def __init__(self, inbounds, configuration : Configuration):
|
||||||
|
self.Configuration = configuration
|
||||||
|
self.ResultDelay = None
|
||||||
|
self.FcfsDelay = None
|
||||||
|
self.Result = None
|
||||||
|
self.Nodes = []
|
||||||
|
|
||||||
|
# create the new planning instances
|
||||||
|
currentTime = dt.utcfromtimestamp(int(time.time())).replace(tzinfo = pytz.UTC)
|
||||||
|
for inbound in inbounds:
|
||||||
|
self.Nodes.append(Node(inbound, currentTime, self.Configuration.WeatherModel, self.Configuration.AirportConfiguration, self.Configuration.RunwayConstraints))
|
||||||
|
|
||||||
|
rwyManager = RunwayManager(self.Configuration)
|
||||||
|
delay = Colony.calculateInitialCosts(rwyManager, self.Nodes, self.Configuration.EarliestArrivalTime)
|
||||||
|
if None == delay:
|
||||||
|
return
|
||||||
|
self.FcfsDelay = delay
|
||||||
|
|
||||||
|
# run the optimization in every cycle to ensure optimal spacings based on TTG
|
||||||
|
if 0.0 >= delay.total_seconds():
|
||||||
|
delay = timedelta(seconds = 1.0)
|
||||||
|
|
||||||
|
# initial value for the optimization
|
||||||
|
self.Configuration.ThetaZero = 1.0 / (len(self.Nodes) * (delay.total_seconds() / 60.0))
|
||||||
|
self.PheromoneMatrix = np.ones(( len(self.Nodes), len(self.Nodes) ), dtype=float) * self.Configuration.ThetaZero
|
||||||
|
|
||||||
|
def sequenceAndPredictInbound(self, rwyManager : RunwayManager, node : Node):
|
||||||
|
self.Result.append(node)
|
||||||
|
Colony.associateInbound(rwyManager, node, self.Configuration.EarliestArrivalTime)
|
||||||
|
|
||||||
|
reqTimeDelta = self.Result[-1].Inbound.EnrouteArrivalTime - self.Result[-1].Inbound.PlannedArrivalTime
|
||||||
|
self.Result[-1].Inbound.PlannedArrivalRoute[0].PTA = self.Result[-1].Inbound.PlannedArrivalRoute[0].ETA - reqTimeDelta
|
||||||
|
for i in range(1, len(self.Result[-1].Inbound.PlannedArrivalRoute)):
|
||||||
|
prev = self.Result[-1].Inbound.PlannedArrivalRoute[i - 1]
|
||||||
|
current = self.Result[-1].Inbound.PlannedArrivalRoute[i]
|
||||||
|
current.PTA = prev.PTA + (current.ETA - prev.ETA)
|
||||||
|
|
||||||
|
def optimize(self):
|
||||||
|
if None == self.FcfsDelay:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# define the tracking variables
|
||||||
|
bestSequence = None
|
||||||
|
|
||||||
|
# run the optimization loops
|
||||||
|
for _ in range(0, self.Configuration.ExplorationRuns):
|
||||||
|
# select the first inbound
|
||||||
|
index = random.randint(1, len(self.Nodes)) - 1
|
||||||
|
candidates = []
|
||||||
|
|
||||||
|
for _ in range(0, self.Configuration.AntCount):
|
||||||
|
# let the ant find a solution
|
||||||
|
ant = Ant(self.PheromoneMatrix, self.Configuration, self.Nodes)
|
||||||
|
ant.findSolution(index)
|
||||||
|
|
||||||
|
# fallback to check if findSolution was successful
|
||||||
|
if None == ant.SequenceDelay or None == ant.Sequence:
|
||||||
|
sys.stderr.write('Invalid ANT run detected!')
|
||||||
|
break
|
||||||
|
|
||||||
|
candidates.append(
|
||||||
|
[
|
||||||
|
ant.SequenceDelay,
|
||||||
|
ant.Sequence
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# find the best solution in all candidates of this generation
|
||||||
|
bestCandidate = None
|
||||||
|
for candidate in candidates:
|
||||||
|
if None == bestCandidate or candidate[0] < bestCandidate[0]:
|
||||||
|
bestCandidate = candidate
|
||||||
|
|
||||||
|
if None != bestSequence:
|
||||||
|
dTheta = 1.0 / ((bestSequence[0].total_seconds() / 60.0) or 1.0)
|
||||||
|
for i in range(1, len(bestSequence[1])):
|
||||||
|
update = (1.0 - self.Configuration.Epsilon) * self.PheromoneMatrix[bestSequence[1][i - 1], bestSequence[1][i]] + self.Configuration.Epsilon * dTheta
|
||||||
|
self.PheromoneMatrix[bestSequence[1][i - 1], bestSequence[1][i]] = max(update, self.Configuration.ThetaZero)
|
||||||
|
|
||||||
|
# check if we find a new best candidate
|
||||||
|
if None != bestCandidate:
|
||||||
|
if None == bestSequence or bestCandidate[0] < bestSequence[0]:
|
||||||
|
bestSequence = bestCandidate
|
||||||
|
|
||||||
|
# found the optimal solution
|
||||||
|
if 1 >= bestSequence[0].total_seconds():
|
||||||
|
break
|
||||||
|
|
||||||
|
# create the final sequence
|
||||||
|
self.Result = []
|
||||||
|
rwyManager = RunwayManager(self.Configuration)
|
||||||
|
|
||||||
|
# use the optimized sequence
|
||||||
|
if None != bestSequence and self.FcfsDelay >= bestSequence[0]:
|
||||||
|
# create the resulting sequence
|
||||||
|
self.ResultDelay = bestSequence[0]
|
||||||
|
|
||||||
|
# finalize the sequence
|
||||||
|
for idx in bestSequence[1]:
|
||||||
|
self.sequenceAndPredictInbound(rwyManager, self.Nodes[idx])
|
||||||
|
# use the FCFS sequence
|
||||||
|
else:
|
||||||
|
self.ResultDelay = self.FcfsDelay
|
||||||
|
for node in self.Nodes:
|
||||||
|
self.sequenceAndPredictInbound(rwyManager, node)
|
||||||
|
|
||||||
|
return True
|
||||||
23
aman/sys/aco/Configuration.py
Normal file
23
aman/sys/aco/Configuration.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
class Configuration:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
# the AMAN specific information
|
||||||
|
self.RunwayConstraints = kwargs.get('constraints', None)
|
||||||
|
self.PreceedingRunwayInbounds = kwargs.get('preceedingRunways', None)
|
||||||
|
self.PreceedingIafInbounds = kwargs.get('preceedingIafs', None)
|
||||||
|
self.EarliestArrivalTime = kwargs.get('earliest', None)
|
||||||
|
self.WeatherModel = kwargs.get('weather', None)
|
||||||
|
self.AirportConfiguration = kwargs.get('config', None)
|
||||||
|
|
||||||
|
# the ACO specific information
|
||||||
|
self.AntCount = kwargs.get('ants', 20)
|
||||||
|
self.ExplorationRuns = kwargs.get('generations', 20)
|
||||||
|
self.PheromoneEvaporationRate = 0.9
|
||||||
|
self.PseudoRandomSelectionRate = 0.9
|
||||||
|
self.PropagationRatio = 0.9
|
||||||
|
self.Epsilon = 0.1
|
||||||
|
self.Beta = 2.0
|
||||||
|
self.ThetaZero = None
|
||||||
71
aman/sys/aco/Constraints.py
Normal file
71
aman/sys/aco/Constraints.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
class SpacingConstraints:
|
||||||
|
def __init__(self):
|
||||||
|
self.WtcSpacing = {}
|
||||||
|
self.WtcSpacing['L'] = {}
|
||||||
|
self.WtcSpacing['M'] = {}
|
||||||
|
self.WtcSpacing['H'] = {}
|
||||||
|
self.WtcSpacing['J'] = {}
|
||||||
|
|
||||||
|
self.WtcSpacing['L']['L'] = 3.0
|
||||||
|
self.WtcSpacing['L']['M'] = 3.0
|
||||||
|
self.WtcSpacing['L']['H'] = 3.0
|
||||||
|
self.WtcSpacing['L']['J'] = 3.0
|
||||||
|
self.WtcSpacing['M']['L'] = 5.0
|
||||||
|
self.WtcSpacing['M']['M'] = 3.0
|
||||||
|
self.WtcSpacing['M']['H'] = 3.0
|
||||||
|
self.WtcSpacing['M']['J'] = 3.0
|
||||||
|
self.WtcSpacing['H']['L'] = 6.0
|
||||||
|
self.WtcSpacing['H']['M'] = 5.0
|
||||||
|
self.WtcSpacing['H']['H'] = 4.0
|
||||||
|
self.WtcSpacing['H']['J'] = 4.0
|
||||||
|
self.WtcSpacing['J']['L'] = 8.0
|
||||||
|
self.WtcSpacing['J']['M'] = 7.0
|
||||||
|
self.WtcSpacing['J']['H'] = 6.0
|
||||||
|
self.WtcSpacing['J']['J'] = 6.0
|
||||||
|
|
||||||
|
self.RecatSpacing = {}
|
||||||
|
self.RecatSpacing['A'] = {}
|
||||||
|
self.RecatSpacing['B'] = {}
|
||||||
|
self.RecatSpacing['C'] = {}
|
||||||
|
self.RecatSpacing['D'] = {}
|
||||||
|
self.RecatSpacing['E'] = {}
|
||||||
|
self.RecatSpacing['F'] = {}
|
||||||
|
|
||||||
|
self.RecatSpacing['A']['A'] = 3.0
|
||||||
|
self.RecatSpacing['A']['B'] = 3.0
|
||||||
|
self.RecatSpacing['A']['C'] = 3.0
|
||||||
|
self.RecatSpacing['A']['D'] = 3.0
|
||||||
|
self.RecatSpacing['A']['E'] = 3.0
|
||||||
|
self.RecatSpacing['A']['F'] = 3.0
|
||||||
|
self.RecatSpacing['B']['A'] = 4.0
|
||||||
|
self.RecatSpacing['B']['B'] = 4.0
|
||||||
|
self.RecatSpacing['B']['C'] = 4.0
|
||||||
|
self.RecatSpacing['B']['D'] = 4.0
|
||||||
|
self.RecatSpacing['B']['E'] = 4.0
|
||||||
|
self.RecatSpacing['B']['F'] = 4.0
|
||||||
|
self.RecatSpacing['C']['A'] = 5.0
|
||||||
|
self.RecatSpacing['C']['B'] = 5.0
|
||||||
|
self.RecatSpacing['C']['C'] = 5.0
|
||||||
|
self.RecatSpacing['C']['D'] = 5.0
|
||||||
|
self.RecatSpacing['C']['E'] = 5.0
|
||||||
|
self.RecatSpacing['C']['F'] = 5.0
|
||||||
|
self.RecatSpacing['D']['A'] = 6.0
|
||||||
|
self.RecatSpacing['D']['B'] = 4.0
|
||||||
|
self.RecatSpacing['D']['C'] = 3.0
|
||||||
|
self.RecatSpacing['D']['D'] = 3.0
|
||||||
|
self.RecatSpacing['D']['E'] = 2.5
|
||||||
|
self.RecatSpacing['D']['F'] = 2.5
|
||||||
|
self.RecatSpacing['E']['A'] = 7.0
|
||||||
|
self.RecatSpacing['E']['B'] = 5.0
|
||||||
|
self.RecatSpacing['E']['C'] = 4.0
|
||||||
|
self.RecatSpacing['E']['D'] = 4.0
|
||||||
|
self.RecatSpacing['E']['E'] = 3.0
|
||||||
|
self.RecatSpacing['E']['F'] = 3.0
|
||||||
|
self.RecatSpacing['F']['A'] = 8.0
|
||||||
|
self.RecatSpacing['F']['B'] = 6.0
|
||||||
|
self.RecatSpacing['F']['C'] = 5.0
|
||||||
|
self.RecatSpacing['F']['D'] = 5.0
|
||||||
|
self.RecatSpacing['F']['E'] = 4.0
|
||||||
|
self.RecatSpacing['F']['F'] = 3.0
|
||||||
222
aman/sys/aco/Node.py
Normal file
222
aman/sys/aco/Node.py
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import math
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from aman.config.Airport import Airport
|
||||||
|
from aman.config.AirportSequencing import AirportSequencing
|
||||||
|
from aman.formats.SctEseFormat import SctEseFormat
|
||||||
|
from aman.sys.WeatherModel import WeatherModel
|
||||||
|
from aman.types.ArrivalData import ArrivalData
|
||||||
|
from aman.types.ArrivalRoute import ArrivalRoute
|
||||||
|
from aman.types.ArrivalWaypoint import ArrivalWaypoint
|
||||||
|
from aman.types.Runway import Runway
|
||||||
|
from aman.types.Inbound import Inbound
|
||||||
|
from aman.types.Waypoint import Waypoint
|
||||||
|
|
||||||
|
class Node:
|
||||||
|
def findArrivalRoute(iaf : str, runway : Runway, navData : SctEseFormat):
|
||||||
|
for arrivalRunway in navData.ArrivalRoutes:
|
||||||
|
if arrivalRunway == runway.Name:
|
||||||
|
stars = navData.ArrivalRoutes[arrivalRunway]
|
||||||
|
for star in stars:
|
||||||
|
if 0 != len(star.Route) and iaf == star.Iaf.Name:
|
||||||
|
return star
|
||||||
|
return None
|
||||||
|
|
||||||
|
def updateArrivalWaypoint(self, arrivalRoute, flightTime, altitude, indicatedAirspeed, groundSpeed):
|
||||||
|
arrivalRoute[-1].FlightTime = timedelta(seconds = flightTime)
|
||||||
|
arrivalRoute[-1].ETA = self.PredictionTime + arrivalRoute[-1].FlightTime
|
||||||
|
arrivalRoute[-1].PTA = arrivalRoute[-1].ETA
|
||||||
|
arrivalRoute[-1].Altitude = altitude
|
||||||
|
arrivalRoute[-1].IndicatedAirspeed = indicatedAirspeed
|
||||||
|
arrivalRoute[-1].GroundSpeed = groundSpeed
|
||||||
|
|
||||||
|
def arrivalEstimation(self, runway : Runway, star : ArrivalRoute, weather : WeatherModel):
|
||||||
|
# calculate remaining trackmiles
|
||||||
|
trackmiles = self.PredictedDistanceToIAF
|
||||||
|
start = star.Route[0]
|
||||||
|
turnIndices = [ -1, -1 ]
|
||||||
|
constraints = []
|
||||||
|
for i in range(0, len(star.Route)):
|
||||||
|
# identified the base turn
|
||||||
|
if True == star.Route[i].BaseTurn:
|
||||||
|
turnIndices[0] = i
|
||||||
|
# identified the final turn
|
||||||
|
elif -1 != turnIndices[0] and True == star.Route[i].FinalTurn:
|
||||||
|
turnIndices[1] = i
|
||||||
|
# skip waypoints until the final turn point is found
|
||||||
|
elif -1 != turnIndices[0] and -1 == turnIndices[1]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
trackmiles += start.haversine(star.Route[i])
|
||||||
|
|
||||||
|
# check if a new constraint is defined
|
||||||
|
altitude = -1
|
||||||
|
speed = -1
|
||||||
|
if None != star.Route[i].Altitude:
|
||||||
|
altitude = star.Route[i].Altitude
|
||||||
|
if None != star.Route[i].Speed:
|
||||||
|
speed = star.Route[i].Speed
|
||||||
|
if -1 != altitude or -1 != speed:
|
||||||
|
constraints.append([ trackmiles, altitude, speed ])
|
||||||
|
|
||||||
|
start = star.Route[i]
|
||||||
|
|
||||||
|
# add the remaining distance from the last waypoint to the runway threshold
|
||||||
|
trackmiles += start.haversine(runway.Start)
|
||||||
|
|
||||||
|
if turnIndices[0] > turnIndices[1] or (-1 == turnIndices[1] and -1 != turnIndices[0]):
|
||||||
|
sys.stderr.write('Invalid constraint definition found for ' + star.Name)
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
# calculate descend profile
|
||||||
|
currentHeading = Waypoint(latitude = self.Inbound.Report.position.latitude, longitude = self.Inbound.Report.position.longitude).bearing(star.Route[0])
|
||||||
|
currentIAS = self.Inbound.PerformanceData.ias(self.Inbound.Report.dynamics.altitude, trackmiles)
|
||||||
|
currentPosition = [ self.Inbound.Report.dynamics.altitude, self.Inbound.Report.dynamics.groundSpeed ]
|
||||||
|
distanceToWaypoint = self.PredictedDistanceToIAF
|
||||||
|
flightTimeSeconds = 0
|
||||||
|
flightTimeOnStarSeconds = 0
|
||||||
|
nextWaypointIndex = 0
|
||||||
|
flownDistance = 0.0
|
||||||
|
arrivalRoute = [ ArrivalWaypoint(waypoint = star.Route[0], trackmiles = distanceToWaypoint) ]
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# check if a constraint cleanup is needed and if a speed-update is needed
|
||||||
|
if 0 != len(constraints) and flownDistance >= constraints[0][0]:
|
||||||
|
if -1 != constraints[0][2]:
|
||||||
|
currentIAS = min(constraints[0][2], self.Inbound.PerformanceData.ias(self.Inbound.Report.dynamics.altitude, trackmiles - flownDistance))
|
||||||
|
currentPosition[1] = min(weather.calculateGS(currentPosition[0], currentIAS, currentHeading), currentPosition[1])
|
||||||
|
constraints.pop(0)
|
||||||
|
|
||||||
|
# search next altitude constraint
|
||||||
|
altitudeDistance = 0
|
||||||
|
nextAltitude = 0
|
||||||
|
for constraint in constraints:
|
||||||
|
if -1 != constraint[1]:
|
||||||
|
altitudeDistance = constraint[0]
|
||||||
|
nextAltitude = constraint[1]
|
||||||
|
break
|
||||||
|
|
||||||
|
# check if update of altitude and speed is needed on 3° glide
|
||||||
|
if currentPosition[0] > nextAltitude and ((currentPosition[0] - nextAltitude) / 1000 * 3) > (altitudeDistance - flownDistance):
|
||||||
|
oldGroundspeed = currentPosition[1]
|
||||||
|
descendRate = (currentPosition[1] / 60) / 3 * 1000 / 6
|
||||||
|
newAltitude = currentPosition[0] - descendRate
|
||||||
|
if 0 > newAltitude:
|
||||||
|
newAltitude = 0
|
||||||
|
|
||||||
|
currentPosition = [ newAltitude, min(weather.calculateGS(newAltitude, currentIAS, currentHeading), currentPosition[1]) ]
|
||||||
|
distance = (currentPosition[1] + oldGroundspeed) / 2 / 60 / 6
|
||||||
|
else:
|
||||||
|
distance = currentPosition[1] / 60 / 6
|
||||||
|
|
||||||
|
# update the statistics
|
||||||
|
distanceToWaypoint -= distance
|
||||||
|
flownDistance += distance
|
||||||
|
newIAS = min(currentIAS, self.Inbound.PerformanceData.ias(currentPosition[0], trackmiles - flownDistance))
|
||||||
|
if newIAS < currentIAS:
|
||||||
|
currentPosition[1] = min(weather.calculateGS(currentPosition[0], newIAS, currentHeading), currentPosition[1])
|
||||||
|
currentIAS = newIAS
|
||||||
|
|
||||||
|
flightTimeSeconds += 10
|
||||||
|
if flownDistance >= self.PredictedDistanceToIAF:
|
||||||
|
flightTimeOnStarSeconds += 10
|
||||||
|
if flownDistance >= trackmiles:
|
||||||
|
if None == arrivalRoute[-1].FlightTime:
|
||||||
|
self.updateArrivalWaypoint(arrivalRoute, flightTimeSeconds, currentPosition[0], currentIAS, currentPosition[1])
|
||||||
|
break
|
||||||
|
|
||||||
|
# check if we follow a new waypoint pair
|
||||||
|
if 0 >= distanceToWaypoint:
|
||||||
|
lastWaypointIndex = nextWaypointIndex
|
||||||
|
nextWaypointIndex += 1
|
||||||
|
|
||||||
|
self.updateArrivalWaypoint(arrivalRoute, flightTimeSeconds, currentPosition[0], currentIAS, currentPosition[1])
|
||||||
|
|
||||||
|
# check if a skip from base to final turn waypoints is needed
|
||||||
|
if -1 != turnIndices[0] and nextWaypointIndex > turnIndices[0] and nextWaypointIndex < turnIndices[1]:
|
||||||
|
nextWaypointIndex = turnIndices[1]
|
||||||
|
|
||||||
|
# update the statistics
|
||||||
|
if nextWaypointIndex < len(star.Route):
|
||||||
|
distanceToWaypoint = star.Route[lastWaypointIndex].haversine(star.Route[nextWaypointIndex])
|
||||||
|
currentHeading = star.Route[lastWaypointIndex].bearing(star.Route[nextWaypointIndex])
|
||||||
|
currentPosition[1] = min(weather.calculateGS(currentPosition[0], currentIAS, currentHeading), currentPosition[1])
|
||||||
|
|
||||||
|
arrivalRoute.append(ArrivalWaypoint(waypoint = star.Route[nextWaypointIndex], trackmiles = arrivalRoute[-1].Trackmiles + distanceToWaypoint))
|
||||||
|
|
||||||
|
return timedelta(seconds = flightTimeSeconds), trackmiles, arrivalRoute, timedelta(seconds = flightTimeOnStarSeconds)
|
||||||
|
|
||||||
|
def __init__(self, inbound : Inbound, referenceTime : datetime, weatherModel : WeatherModel,
|
||||||
|
airportConfig : Airport, sequencingConfig : AirportSequencing):
|
||||||
|
self.PredictedDistanceToIAF = inbound.Report.distanceToIAF
|
||||||
|
self.PredictedCoordinate = [ inbound.CurrentPosition.latitude, inbound.CurrentPosition.longitude ]
|
||||||
|
self.PredictionTime = referenceTime
|
||||||
|
self.ArrivalCandidates = None
|
||||||
|
self.Inbound = inbound
|
||||||
|
|
||||||
|
if None == referenceTime or None == sequencingConfig:
|
||||||
|
return
|
||||||
|
|
||||||
|
# predict the distance to IAF
|
||||||
|
timePrediction = (referenceTime - inbound.ReportTime).total_seconds()
|
||||||
|
if 0 != timePrediction and 0 != len(sequencingConfig.ActiveArrivalRunways):
|
||||||
|
# calculate current motion information
|
||||||
|
course = weatherModel.estimateCourse(inbound.Report.dynamics.altitude, inbound.Report.dynamics.groundSpeed, inbound.Report.dynamics.heading)
|
||||||
|
tempWaypoint = Waypoint(longitude = inbound.CurrentPosition.longitude, latitude = inbound.CurrentPosition.latitude)
|
||||||
|
gs = inbound.Report.dynamics.groundSpeed * 0.514444 # ground speed in m/s
|
||||||
|
distance = gs * timePrediction
|
||||||
|
prediction = tempWaypoint.project(course, distance)
|
||||||
|
|
||||||
|
# calculate the bearing between the current position and the IAF
|
||||||
|
star = Node.findArrivalRoute(inbound.Report.initialApproachFix, sequencingConfig.ActiveArrivalRunways[0].Runway, airportConfig.GngData)
|
||||||
|
|
||||||
|
# calculate the distance based on the flown distance and update the predicted distance
|
||||||
|
if None != star:
|
||||||
|
bearing = Waypoint(longitude = prediction[1], latitude = prediction[0]).bearing(star.Route[0])
|
||||||
|
correctedDistance = math.cos(abs(bearing - course)) * distance * 0.000539957
|
||||||
|
self.PredictedDistanceToIAF -= correctedDistance
|
||||||
|
if 0.0 > self.PredictedDistanceToIAF:
|
||||||
|
self.PredictedDistanceToIAF = 0.0
|
||||||
|
|
||||||
|
self.PredictedCoordinate = prediction
|
||||||
|
|
||||||
|
setEnrouteTime = None == self.Inbound.EnrouteArrivalTime
|
||||||
|
self.ArrivalCandidates = {}
|
||||||
|
|
||||||
|
# calculate the timings for the different arrival runways
|
||||||
|
for identifier in sequencingConfig.ActiveArrivalRunways:
|
||||||
|
star = Node.findArrivalRoute(self.Inbound.Report.initialApproachFix, identifier.Runway, airportConfig.GngData)
|
||||||
|
|
||||||
|
if None != star:
|
||||||
|
flightTime, trackmiles, arrivalRoute, flightTimeOnStar = self.arrivalEstimation(identifier.Runway, star, weatherModel)
|
||||||
|
|
||||||
|
# use the the distance to the IAF for optimizations
|
||||||
|
timeUntilIAF = flightTime - flightTimeOnStar
|
||||||
|
if 0.0 > timeUntilIAF.total_seconds():
|
||||||
|
timeUntilIAF = timedelta(seconds = 0)
|
||||||
|
|
||||||
|
# the best TTL is the longest path with the slowest speed
|
||||||
|
ttgMax = 60
|
||||||
|
ttgRatio = 0.05
|
||||||
|
if star.Name in airportConfig.OptimizationParameters:
|
||||||
|
ttgMax = airportConfig.OptimizationParameters[star.Name][0]
|
||||||
|
ttgRatio = airportConfig.OptimizationParameters[star.Name][1]
|
||||||
|
|
||||||
|
ttg = timedelta(seconds = timeUntilIAF.total_seconds() * ttgRatio)
|
||||||
|
if (ttg.total_seconds() > ttgMax):
|
||||||
|
ttg = timedelta(seconds = ttgMax)
|
||||||
|
if None == self.Inbound.MaximumTimeToGain or ttg > self.Inbound.MaximumTimeToGain:
|
||||||
|
self.Inbound.MaximumTimeToGain = ttg
|
||||||
|
|
||||||
|
ita = self.Inbound.ReportTime + flightTime
|
||||||
|
earliest = ita - self.Inbound.MaximumTimeToGain
|
||||||
|
|
||||||
|
self.ArrivalCandidates[identifier.Runway.Name] = ArrivalData(star = star, ita = earliest, route = arrivalRoute,
|
||||||
|
trackmiles = trackmiles)
|
||||||
|
|
||||||
|
if True == setEnrouteTime and (None == self.Inbound.EnrouteArrivalTime or ita < self.Inbound.EnrouteArrivalTime):
|
||||||
|
self.Inbound.EnrouteArrivalTime = ita
|
||||||
208
aman/sys/aco/RunwayManager.py
Normal file
208
aman/sys/aco/RunwayManager.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from aman.config.RunwaySequencing import RunwayAssignmentType
|
||||||
|
from aman.sys.aco.Configuration import Configuration
|
||||||
|
from aman.sys.aco.Constraints import SpacingConstraints
|
||||||
|
from aman.sys.aco.Node import Node
|
||||||
|
|
||||||
|
class RunwayManager:
|
||||||
|
def __init__(self, configuration : Configuration):
|
||||||
|
self.Spacings = SpacingConstraints()
|
||||||
|
self.Configuration = configuration
|
||||||
|
self.RunwayInbounds = copy.deepcopy(configuration.PreceedingRunwayInbounds)
|
||||||
|
self.IafInbounds = copy.deepcopy(configuration.PreceedingIafInbounds)
|
||||||
|
|
||||||
|
def calculateEarliestArrivalTime(self, runway : str, node : Node, earliestArrivalTime : datetime):
|
||||||
|
constrainedETA = None
|
||||||
|
|
||||||
|
if None != self.RunwayInbounds[runway]:
|
||||||
|
# get the WTC based ETA
|
||||||
|
if None == self.RunwayInbounds[runway].Inbound.WTC or None == node.Inbound.WTC:
|
||||||
|
spacingWTC = 3
|
||||||
|
else:
|
||||||
|
if self.RunwayInbounds[runway].Inbound.WTC not in self.Spacings.WtcSpacing:
|
||||||
|
spacingWTC = 3
|
||||||
|
elif node.Inbound.WTC not in self.Spacings.WtcSpacing[self.RunwayInbounds[runway].Inbound.WTC]:
|
||||||
|
spacingWTC = self.Spacings.WtcSpacing[self.RunwayInbounds[runway].Inbound.WTC]['L']
|
||||||
|
else:
|
||||||
|
spacingWTC = self.Spacings.WtcSpacing[self.RunwayInbounds[runway].Inbound.WTC][node.Inbound.WTC]
|
||||||
|
|
||||||
|
# get the runway time spacing
|
||||||
|
spacingRunway = self.Configuration.RunwayConstraints.findRunway(runway).Spacing
|
||||||
|
constrainedETA = self.RunwayInbounds[runway].Inbound.PlannedArrivalTime + timedelta(minutes = max(spacingWTC, spacingRunway) / (node.Inbound.PerformanceData.SpeedApproach / 60))
|
||||||
|
|
||||||
|
# calculate the arrival times for the dependent inbounds
|
||||||
|
for dependentRunway in self.Configuration.RunwayConstraints.findDependentRunways(runway):
|
||||||
|
if None != self.RunwayInbounds[dependentRunway.Runway.Name]:
|
||||||
|
candidate = self.RunwayInbounds[dependentRunway.Runway.Name].Inbound.PlannedArrivalTime + timedelta(minutes = 3 / (node.Inbound.PerformanceData.SpeedApproach / 60))
|
||||||
|
if None == constrainedETA or candidate > constrainedETA:
|
||||||
|
constrainedETA = candidate
|
||||||
|
|
||||||
|
if None == constrainedETA:
|
||||||
|
eta = max(node.ArrivalCandidates[runway].InitialArrivalTime, earliestArrivalTime)
|
||||||
|
else:
|
||||||
|
eta = max(node.ArrivalCandidates[runway].InitialArrivalTime, max(constrainedETA, earliestArrivalTime))
|
||||||
|
|
||||||
|
return eta, eta - node.ArrivalCandidates[runway].InitialArrivalTime
|
||||||
|
|
||||||
|
def selectShallShouldMayArrivalRunway(self, node : Node, runways, earliestArrivalTime : datetime):
|
||||||
|
candidate = None
|
||||||
|
delay = None
|
||||||
|
|
||||||
|
for runway in runways:
|
||||||
|
eta, _ = self.calculateEarliestArrivalTime(runway.Runway.Name, node, earliestArrivalTime)
|
||||||
|
if None == delay:
|
||||||
|
delay = eta - node.ArrivalCandidates[runway.Runway.Name].InitialArrivalTime
|
||||||
|
candidate = runway
|
||||||
|
elif delay > (eta - node.ArrivalCandidates[runway.Runway.Name].InitialArrivalTime):
|
||||||
|
delay = eta- node.ArrivalCandidates[runway.Runway.Name].InitialArrivalTime
|
||||||
|
candidate = runway
|
||||||
|
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
def executeShallShouldMayAssignment(self, node : Node, earliestArrivalTime : datetime):
|
||||||
|
shallRunways = []
|
||||||
|
shouldRunways = []
|
||||||
|
mayRunways = []
|
||||||
|
expectedRunway = None
|
||||||
|
|
||||||
|
for runway in self.Configuration.RunwayConstraints.ActiveArrivalRunways:
|
||||||
|
# test the shall assignments
|
||||||
|
if RunwayAssignmentType.AircraftType in runway.ShallAssignments:
|
||||||
|
if node.Inbound.Report.aircraft.type in runway.ShallAssignments[RunwayAssignmentType.AircraftType]:
|
||||||
|
shallRunways.append(runway)
|
||||||
|
expectedRunway = runway.Runway.Name
|
||||||
|
if RunwayAssignmentType.GateAssignment in runway.ShallAssignments:
|
||||||
|
if node.Inbound.Report.plannedGate in runway.ShallAssignments[RunwayAssignmentType.GateAssignment]:
|
||||||
|
shallRunways.append(runway)
|
||||||
|
expectedRunway = runway.Runway.Name
|
||||||
|
|
||||||
|
# test the should assignments
|
||||||
|
if RunwayAssignmentType.AircraftType in runway.ShouldAssignments:
|
||||||
|
if node.Inbound.Report.aircraft.type in runway.ShouldAssignments[RunwayAssignmentType.AircraftType]:
|
||||||
|
shouldRunways.append(runway)
|
||||||
|
expectedRunway = runway.Runway.Name
|
||||||
|
if RunwayAssignmentType.GateAssignment in runway.ShouldAssignments:
|
||||||
|
if node.Inbound.Report.plannedGate in runway.ShouldAssignments[RunwayAssignmentType.GateAssignment]:
|
||||||
|
shouldRunways.append(runway)
|
||||||
|
expectedRunway = runway.Runway.Name
|
||||||
|
|
||||||
|
# test the may assignments
|
||||||
|
if RunwayAssignmentType.AircraftType in runway.MayAssignments:
|
||||||
|
if node.Inbound.Report.aircraft.type in runway.MayAssignments[RunwayAssignmentType.AircraftType]:
|
||||||
|
eta, _ = self.calculateEarliestArrivalTime(runway.Runway.Name, node, earliestArrivalTime)
|
||||||
|
if (eta - node.ArrivalCandidates[runway.Runway.Name].InitialArrivalTime) <= self.Configuration.AirportConfiguration.MaxDelayMay:
|
||||||
|
mayRunways.append(runway)
|
||||||
|
expectedRunway = runway.Runway.Name
|
||||||
|
if RunwayAssignmentType.GateAssignment in runway.MayAssignments:
|
||||||
|
if node.Inbound.Report.plannedGate in runway.MayAssignments[RunwayAssignmentType.GateAssignment]:
|
||||||
|
eta, _ = self.calculateEarliestArrivalTime(runway.Runway.Name, node, earliestArrivalTime)
|
||||||
|
if (eta - node.ArrivalCandidates[runway.Runway.Name].InitialArrivalTime) <= self.Configuration.AirportConfiguration.MaxDelayMay:
|
||||||
|
mayRunways.append(runway)
|
||||||
|
expectedRunway = runway.Runway.Name
|
||||||
|
|
||||||
|
runway = self.selectShallShouldMayArrivalRunway(node, shallRunways, earliestArrivalTime)
|
||||||
|
if None != runway:
|
||||||
|
return 'shall', expectedRunway, [ runway ]
|
||||||
|
runway = self.selectShallShouldMayArrivalRunway(node, shouldRunways, earliestArrivalTime)
|
||||||
|
if None != runway:
|
||||||
|
return 'should', expectedRunway, [ runway ]
|
||||||
|
runway = self.selectShallShouldMayArrivalRunway(node, mayRunways, earliestArrivalTime)
|
||||||
|
if None != runway:
|
||||||
|
return 'may', expectedRunway, [ runway ]
|
||||||
|
|
||||||
|
return 'other', None, self.Configuration.RunwayConstraints.ActiveArrivalRunways
|
||||||
|
|
||||||
|
def selectArrivalRunway(self, node : Node, earliestArrivalTime : datetime):
|
||||||
|
availableRunways = self.Configuration.RunwayConstraints.ActiveArrivalRunways
|
||||||
|
if 0 == len(availableRunways):
|
||||||
|
return None, None, None, None, None
|
||||||
|
|
||||||
|
expectedRunway = None
|
||||||
|
|
||||||
|
if True == self.Configuration.RunwayConstraints.UseShallShouldMay and None == node.Inbound.RequestedRunway:
|
||||||
|
type, expectedRunway, availableRunways = self.executeShallShouldMayAssignment(node, earliestArrivalTime)
|
||||||
|
elif None != node.Inbound.RequestedRunway:
|
||||||
|
for runway in availableRunways:
|
||||||
|
if node.Inbound.RequestedRunway == runway.Runway.Name:
|
||||||
|
availableRunways = [ runway ]
|
||||||
|
type = 'other'
|
||||||
|
break
|
||||||
|
|
||||||
|
if 0 == len(availableRunways):
|
||||||
|
runway = self.Configuration.RunwayConstraints.ActiveArrivalRunways[0]
|
||||||
|
eta, delta = self.calculateEarliestArrivalTime(runway.Runway.Name, node, earliestArrivalTime)
|
||||||
|
return 'other', None, runway, eta, delta
|
||||||
|
|
||||||
|
# start with the beginning
|
||||||
|
selectedRunway = None
|
||||||
|
lostTime = None
|
||||||
|
eta = None
|
||||||
|
|
||||||
|
# get the runway with the earliest ETA
|
||||||
|
for runway in availableRunways:
|
||||||
|
candidate, delta = self.calculateEarliestArrivalTime(runway.Runway.Name, node, earliestArrivalTime)
|
||||||
|
if None == eta or eta > candidate:
|
||||||
|
selectedRunway = runway.Runway
|
||||||
|
lostTime = delta
|
||||||
|
eta = candidate
|
||||||
|
|
||||||
|
# find the corresponding IAF
|
||||||
|
iaf = node.ArrivalCandidates[selectedRunway.Name].ArrivalRoute[0].Waypoint.Name
|
||||||
|
if iaf in self.IafInbounds:
|
||||||
|
delta = 100000.0
|
||||||
|
targetLevel = None
|
||||||
|
|
||||||
|
# find the planned level
|
||||||
|
for level in self.IafInbounds[iaf]:
|
||||||
|
difference = abs(level - node.ArrivalCandidates[selectedRunway.Name].ArrivalRoute[0].Altitude)
|
||||||
|
if difference < delta:
|
||||||
|
delta = difference
|
||||||
|
targetLevel = level
|
||||||
|
|
||||||
|
if targetLevel in self.IafInbounds[iaf]:
|
||||||
|
# check if we have to lose time to ensure the IAF spacing
|
||||||
|
# the function assumes that model allows only TTG during flight to IAF
|
||||||
|
if None != self.IafInbounds[iaf][targetLevel]:
|
||||||
|
if None != self.IafInbounds[iaf][targetLevel].Inbound.PlannedArrivalRoute:
|
||||||
|
# ETA at IAF of preceeding traffic
|
||||||
|
plannedDelta = self.IafInbounds[iaf][targetLevel].Inbound.PlannedArrivalTime - self.IafInbounds[iaf][targetLevel].Inbound.EnrouteArrivalTime
|
||||||
|
iafETAPreceeding = self.IafInbounds[iaf][targetLevel].Inbound.PlannedArrivalRoute[0].ETA + plannedDelta
|
||||||
|
|
||||||
|
# ETA at IAF of current inbound
|
||||||
|
plannedDelta = eta - node.Inbound.EnrouteArrivalTime
|
||||||
|
iafETACurrent = node.ArrivalCandidates[selectedRunway.Name].ArrivalRoute[0].ETA
|
||||||
|
|
||||||
|
# required time delte to ensure IAF spacing
|
||||||
|
timeSpacing = timedelta(hours = self.Configuration.AirportConfiguration.IafSpacing / node.ArrivalCandidates[selectedRunway.Name].ArrivalRoute[0].GroundSpeed)
|
||||||
|
|
||||||
|
# we are too close to preceeding traffic
|
||||||
|
currentTimeSpacing = iafETACurrent - iafETAPreceeding
|
||||||
|
if timeSpacing > currentTimeSpacing:
|
||||||
|
eta = eta + (timeSpacing - currentTimeSpacing)
|
||||||
|
lostTime += (timeSpacing - currentTimeSpacing)
|
||||||
|
|
||||||
|
return type, expectedRunway, selectedRunway, eta, lostTime
|
||||||
|
|
||||||
|
def registerNode(self, node : Node, runway : str):
|
||||||
|
self.RunwayInbounds[runway] = node
|
||||||
|
|
||||||
|
# find the corresponding IAF
|
||||||
|
iaf = node.ArrivalCandidates[runway].ArrivalRoute[0].Waypoint.Name
|
||||||
|
if iaf in self.IafInbounds:
|
||||||
|
delta = 100000.0
|
||||||
|
targetLevel = None
|
||||||
|
|
||||||
|
# find the planned level
|
||||||
|
for level in self.IafInbounds[iaf]:
|
||||||
|
difference = abs(level - node.ArrivalCandidates[runway].ArrivalRoute[0].Altitude)
|
||||||
|
if difference < delta:
|
||||||
|
delta = difference
|
||||||
|
targetLevel = level
|
||||||
|
|
||||||
|
if targetLevel in self.IafInbounds[iaf]:
|
||||||
|
self.IafInbounds[iaf][targetLevel] = node
|
||||||
0
aman/sys/aco/__init__.py
Normal file
0
aman/sys/aco/__init__.py
Normal file
@@ -1,7 +1,9 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
from datetime import datetime
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
import zmq.auth
|
import zmq.auth
|
||||||
|
|
||||||
@@ -10,14 +12,12 @@ import zmq.auth
|
|||||||
# @return The public and private key tuple
|
# @return The public and private key tuple
|
||||||
def KeyPairCreator(directory: str, server: bool) -> Tuple[str, str]:
|
def KeyPairCreator(directory: str, server: bool) -> Tuple[str, str]:
|
||||||
if not server:
|
if not server:
|
||||||
print('Creating a new pair for a client...')
|
|
||||||
target = 'client'
|
target = 'client'
|
||||||
else:
|
else:
|
||||||
print('Creating a new pair for the server...')
|
|
||||||
target = 'server'
|
target = 'server'
|
||||||
|
|
||||||
public, private = zmq.auth.create_certificates(directory, target)
|
public, private = zmq.auth.create_certificates(directory, target)
|
||||||
return (public, private)
|
return public, private
|
||||||
|
|
||||||
def str2bool(value):
|
def str2bool(value):
|
||||||
if isinstance(value, bool):
|
if isinstance(value, bool):
|
||||||
@@ -29,16 +29,71 @@ def str2bool(value):
|
|||||||
else:
|
else:
|
||||||
raise argparse.ArgumentTypeError('Boolean value expected')
|
raise argparse.ArgumentTypeError('Boolean value expected')
|
||||||
|
|
||||||
|
def findIdentificationKey(path, publicKey : bool):
|
||||||
|
if True == publicKey:
|
||||||
|
identifier = 'public-key = '
|
||||||
|
else:
|
||||||
|
identifier = 'secret-key = '
|
||||||
|
|
||||||
|
with open(path) as file:
|
||||||
|
key = ''
|
||||||
|
|
||||||
|
for line in file:
|
||||||
|
if identifier in line:
|
||||||
|
elements = line.split('=')
|
||||||
|
for idx in range(1, len(elements)):
|
||||||
|
if 0 == len(key):
|
||||||
|
key = elements[idx][2:-1]
|
||||||
|
key = key + elements[idx][-1]
|
||||||
|
else:
|
||||||
|
key = key + '=' + elements[idx]
|
||||||
|
|
||||||
|
return key[0:-2]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# create the commandline parser
|
# create the commandline parser
|
||||||
parser = argparse.ArgumentParser(description='Create a new key-value pair')
|
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('--directory', type=str, help='Directory where to store the key pair')
|
||||||
|
parser.add_argument('--publickey', nargs='?', type=str, default=os.getcwd(), help='Full path to the public key of the server')
|
||||||
parser.add_argument('--server', default=False, action='store_true', help="Creates server key pair")
|
parser.add_argument('--server', default=False, action='store_true', help="Creates server key pair")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# validate the arguments
|
||||||
|
if False == args.server and not os.path.exists(args.publickey):
|
||||||
|
sys.stderr.write('The public key of the server cannot be found')
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
# create the directory if it does not exist
|
# create the directory if it does not exist
|
||||||
if not os.path.exists(args.directory):
|
if not os.path.exists(args.directory):
|
||||||
os.makedirs(args.directory)
|
os.makedirs(args.directory)
|
||||||
|
|
||||||
# create the keys
|
# create the keys
|
||||||
KeyPairCreator(args.directory, args.server)
|
_, private = KeyPairCreator(args.directory, args.server)
|
||||||
|
|
||||||
|
if False == args.server:
|
||||||
|
publicServer = findIdentificationKey(args.publickey, True)
|
||||||
|
publicClient = findIdentificationKey(private, True)
|
||||||
|
privateClient = findIdentificationKey(private, False)
|
||||||
|
|
||||||
|
if None == publicServer:
|
||||||
|
sys.stderr.write('The public key of the server cannot be found in the defined file')
|
||||||
|
sys.exit(-1)
|
||||||
|
if None == publicClient:
|
||||||
|
sys.stderr.write('Unable to extract the created public key')
|
||||||
|
sys.exit(-1)
|
||||||
|
if None == privateClient:
|
||||||
|
sys.stderr.write('Unable to extract the created private key')
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
# rename keys
|
||||||
|
timestamp = str(datetime.now(tz=None))
|
||||||
|
timestamp = timestamp.replace(' ', '_')
|
||||||
|
timestamp = timestamp.replace(':', '-')
|
||||||
|
os.rename(os.path.join(args.directory, 'client.key'), os.path.join(args.directory, timestamp + '.key'))
|
||||||
|
os.rename(os.path.join(args.directory, 'client.key_secret'), os.path.join(args.directory, timestamp + '.key_secret'))
|
||||||
|
|
||||||
|
print(publicServer)
|
||||||
|
print(publicClient)
|
||||||
|
print(privateClient)
|
||||||
|
|||||||
@@ -118,13 +118,9 @@ if __name__ == '__main__':
|
|||||||
print('Found ' + str(len(links)) + ' aircrafts')
|
print('Found ' + str(len(links)) + ' aircrafts')
|
||||||
|
|
||||||
aircrafts = []
|
aircrafts = []
|
||||||
parsed = 0
|
|
||||||
for link in links:
|
for link in links:
|
||||||
valid, aircraft = parsePerformanceData(link)
|
valid, aircraft = parsePerformanceData(link)
|
||||||
|
|
||||||
parsed += 1
|
|
||||||
print('Parsed ' + str(parsed) + ' of ' + str(len(links)), end='\r')
|
|
||||||
|
|
||||||
if False == valid:
|
if False == valid:
|
||||||
print('Unable to find performance data for ' + link)
|
print('Unable to find performance data for ' + link)
|
||||||
continue
|
continue
|
||||||
|
|||||||
30
aman/types/ArrivalData.py
Normal file
30
aman/types/ArrivalData.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from aman.types.ArrivalRoute import ArrivalRoute
|
||||||
|
|
||||||
|
class ArrivalData:
|
||||||
|
def __init__(self, **kargs):
|
||||||
|
self.Star = None
|
||||||
|
self.InitialArrivalTime = None
|
||||||
|
self.ArrivalRoute = None
|
||||||
|
self.Trackmiles = None
|
||||||
|
|
||||||
|
for key, value in kargs.items():
|
||||||
|
if 'star' == key:
|
||||||
|
if True == isinstance(value, ArrivalRoute):
|
||||||
|
self.Star = value
|
||||||
|
else:
|
||||||
|
raise Exception('Invalid type for star')
|
||||||
|
elif 'ita' == key:
|
||||||
|
if True == isinstance(value, datetime):
|
||||||
|
self.InitialArrivalTime = value
|
||||||
|
else:
|
||||||
|
raise Exception('Invalid type for ita')
|
||||||
|
elif 'route' == key:
|
||||||
|
self.ArrivalRoute = value
|
||||||
|
elif 'trackmiles' == key:
|
||||||
|
self.Trackmiles = value
|
||||||
|
else:
|
||||||
|
raise Exception('Unknown key: ' + key)
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
class ArrivalRoute:
|
class ArrivalRoute:
|
||||||
def __init__(self, name : str, runway : str, waypoints : list):
|
def __init__(self, name : str, runway : str, waypoints : list):
|
||||||
self.name = name
|
self.Name = name
|
||||||
self.runway = runway
|
self.Runway = runway
|
||||||
self.iaf = waypoints[0]
|
self.Iaf = waypoints[0]
|
||||||
self.route = waypoints
|
self.Route = waypoints
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return 'Name: ' + self.name + ', IAF: ' + self.iaf.name + ', RWY: ' + self.runway
|
return 'Name: ' + self.Name + ', IAF: ' + self.Iaf.Name + ', RWY: ' + self.Runway
|
||||||
34
aman/types/ArrivalWaypoint.py
Normal file
34
aman/types/ArrivalWaypoint.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
class ArrivalWaypoint():
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.Waypoint = None
|
||||||
|
self.FlightTime = None
|
||||||
|
self.Trackmiles = None
|
||||||
|
self.IndicatedAirspeed = None
|
||||||
|
self.GroundSpeed = None
|
||||||
|
self.Altitude = None
|
||||||
|
self.ETA = None
|
||||||
|
self.PTA = None
|
||||||
|
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
if 'waypoint' == key.lower():
|
||||||
|
self.Waypoint = value
|
||||||
|
elif 'flighttime' == key.lower():
|
||||||
|
self.FlightTime = value
|
||||||
|
elif 'eta' == key.lower():
|
||||||
|
self.ETA = value
|
||||||
|
elif 'pta' == key.lower():
|
||||||
|
self.PTA = value
|
||||||
|
elif 'trackmiles' == key.lower():
|
||||||
|
self.Trackmiles = value
|
||||||
|
elif 'altitude' == key.lower():
|
||||||
|
self.Altitude = value
|
||||||
|
elif 'groundspeed' == key.lower():
|
||||||
|
self.GroundSpeed = value
|
||||||
|
elif 'indicated' == key.lower():
|
||||||
|
self.IndicatedAirspeed = value
|
||||||
|
else:
|
||||||
|
raise Exception('Invalid constructor argument: ' + key)
|
||||||
|
|
||||||
|
|
||||||
@@ -1,7 +1,45 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from aman.com import AircraftReport_pb2
|
from aman.com import AircraftReport_pb2
|
||||||
|
from aman.sys.WeatherModel import WeatherModel
|
||||||
|
from aman.types.PerformanceData import PerformanceData
|
||||||
|
|
||||||
class Inbound:
|
class Inbound:
|
||||||
def __init__(self, report : AircraftReport_pb2.AircraftReport):
|
def __init__(self, report : AircraftReport_pb2.AircraftReport, performanceData : PerformanceData):
|
||||||
self.report = report
|
self.Report = report
|
||||||
|
self.Callsign = report.aircraft.callsign
|
||||||
|
self.CurrentPosition = report.position
|
||||||
|
self.ReportTime = datetime.strptime(report.reportTime + '+0000', '%Y%m%d%H%M%S%z').replace(tzinfo = pytz.UTC)
|
||||||
|
self.EnrouteArrivalTime = None
|
||||||
|
self.InitialArrivalTime = None
|
||||||
|
self.RequestedRunway = None
|
||||||
|
self.MaximumTimeToGain = None
|
||||||
|
self.PlannedArrivalTime = None
|
||||||
|
self.PlannedRunway = None
|
||||||
|
self.PlannedStar = None
|
||||||
|
self.PlannedArrivalRoute = None
|
||||||
|
self.PlannedTrackmiles = None
|
||||||
|
self.FixedSequence = False
|
||||||
|
self.ExpectedRunway = None
|
||||||
|
self.AssignmentMode = None
|
||||||
|
self.HasValidSequence = False
|
||||||
|
self.WTC = None
|
||||||
|
|
||||||
|
# analyze the WTC
|
||||||
|
wtc = report.aircraft.wtc.upper()
|
||||||
|
if 'L' == wtc or 'M' == wtc or 'H' == wtc or 'J' == wtc:
|
||||||
|
self.WTC = wtc
|
||||||
|
|
||||||
|
# analyze the requested runway
|
||||||
|
if '' != report.requestedRunway:
|
||||||
|
self.RequestedRunway = report.requestedRunway
|
||||||
|
|
||||||
|
# search performance data -> fallback to A320
|
||||||
|
if self.Report.aircraft.type in performanceData.Aircrafts:
|
||||||
|
self.PerformanceData = performanceData.Aircrafts[self.Report.aircraft.type]
|
||||||
|
if None == self.PerformanceData:
|
||||||
|
self.PerformanceData = performanceData.Aircrafts['A320']
|
||||||
|
|||||||
@@ -2,17 +2,29 @@
|
|||||||
|
|
||||||
class PerformanceData:
|
class PerformanceData:
|
||||||
def __init__(self, icao : str):
|
def __init__(self, icao : str):
|
||||||
self.icao = icao
|
self.Icao = icao
|
||||||
self.speedAboveFL240 = 0.0
|
self.SpeedAboveFL240 = 0.0
|
||||||
self.speedAboveFL100 = 0.0
|
self.SpeedAboveFL100 = 0.0
|
||||||
self.speedBelowFL100 = 0.0
|
self.SpeedBelowFL100 = 0.0
|
||||||
self.speedApproach = 0.0
|
self.SpeedApproach = 0.0
|
||||||
self.rodAboveFL240 = 0.0
|
self.RodAboveFL240 = 0.0
|
||||||
self.rodAboveFL100 = 0.0
|
self.RodAboveFL100 = 0.0
|
||||||
self.rodBelowFL100 = 2000.0
|
self.RodBelowFL100 = 2000.0
|
||||||
|
|
||||||
|
def ias(self, altitude, distance):
|
||||||
|
if 24000 < altitude:
|
||||||
|
return self.SpeedAboveFL240
|
||||||
|
elif 10000 < altitude:
|
||||||
|
return self.SpeedAboveFL100
|
||||||
|
elif 10000 >= altitude and 5 < distance:
|
||||||
|
return self.SpeedBelowFL100
|
||||||
|
elif 5 >= distance:
|
||||||
|
return self.SpeedApproach
|
||||||
|
else:
|
||||||
|
return self.SpeedBelowFL100
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return 'ICAO: ' + self.icao + ', FL240: ' + str(self.rodAboveFL240) + '@' + str(self.speedAboveFL240) + \
|
return 'ICAO: ' + self.icao + ', FL240: ' + str(self.RodAboveFL240) + '@' + str(self.SpeedAboveFL240) + \
|
||||||
', +FL100: ' + str(self.rodAboveFL100) + '@' + str(self.speedAboveFL100) + \
|
', +FL100: ' + str(self.RodAboveFL100) + '@' + str(self.SpeedAboveFL100) + \
|
||||||
', -FL100: ' + str(self.rodBelowFL100) + '@' + str(self.speedBelowFL100) + \
|
', -FL100: ' + str(self.RodBelowFL100) + '@' + str(self.SpeedBelowFL100) + \
|
||||||
', Vapp: ' + str(self.speedApproach)
|
', Vapp: ' + str(self.SpeedApproach)
|
||||||
|
|||||||
12
aman/types/Runway.py
Normal file
12
aman/types/Runway.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from aman.types.Waypoint import Waypoint
|
||||||
|
|
||||||
|
class Runway:
|
||||||
|
def __init__(self, start : Waypoint, end : Waypoint):
|
||||||
|
self.Name = start.Name
|
||||||
|
self.Start = start
|
||||||
|
self.End = end
|
||||||
|
|
||||||
|
def heading(self):
|
||||||
|
return self.Start.bearing(self.End)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
from sklearn.metrics.pairwise import haversine_distances
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import pyproj
|
||||||
|
|
||||||
class Waypoint:
|
class Waypoint:
|
||||||
def dms2dd(coordinate : str):
|
def dms2dd(coordinate : str):
|
||||||
@@ -20,14 +20,59 @@ class Waypoint:
|
|||||||
|
|
||||||
return dd
|
return dd
|
||||||
|
|
||||||
def __init__(self, name : str, latitude : float, longitude : float):
|
def coordinateArgument(value):
|
||||||
self.name = name
|
if True == isinstance(value, str):
|
||||||
self.coordinate = np.array([ latitude, longitude ])
|
return Waypoint.dms2dd(value)
|
||||||
|
elif True == isinstance(value, (float, int)):
|
||||||
|
return float(value)
|
||||||
|
else:
|
||||||
|
raise Exception('Invalid constructor argument')
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.Name = None
|
||||||
|
self.Coordinate = None
|
||||||
|
self.Altitude = None
|
||||||
|
self.Speed = None
|
||||||
|
self.BaseTurn = False
|
||||||
|
self.FinalTurn = False
|
||||||
|
|
||||||
|
latitude = None
|
||||||
|
longitude = None
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
if 'name' == key.lower():
|
||||||
|
self.Name = str(value)
|
||||||
|
elif 'latitude' == key.lower():
|
||||||
|
latitude = Waypoint.coordinateArgument(value)
|
||||||
|
elif 'longitude' == key.lower():
|
||||||
|
longitude = Waypoint.coordinateArgument(value)
|
||||||
|
elif 'altitude' == key.lower():
|
||||||
|
self.Altitude = int(value)
|
||||||
|
elif 'speed' == key.lower():
|
||||||
|
self.Speed = int(value)
|
||||||
|
elif 'base' == key:
|
||||||
|
self.BaseTurn = bool(value)
|
||||||
|
elif 'final' == key:
|
||||||
|
self.FinalTurn = bool(value)
|
||||||
|
else:
|
||||||
|
raise Exception('Invalid constructor argument: ' + key)
|
||||||
|
|
||||||
|
if None != latitude and None != longitude:
|
||||||
|
self.Coordinate = np.array([ latitude, longitude ])
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return 'Name: ' + self.name + ', Lat: ' + str(self.coordinate[0]) + ', Lon: ' + str(self.coordinate[1])
|
return 'Name: ' + self.Name + ', Lat: ' + str(self.Coordinate[0]) + ', Lon: ' + str(self.Coordinate[1])
|
||||||
|
|
||||||
def haversine(self, other):
|
def haversine(self, other):
|
||||||
self_radians = [np.radians(_) for _ in self.coordinate]
|
geodesic = pyproj.Geod(ellps='WGS84')
|
||||||
other_radians = [np.radians(_) for _ in other.coordinate]
|
_, _, distance = geodesic.inv(self.Coordinate[1], self.Coordinate[0], other.Coordinate[1], other.Coordinate[0])
|
||||||
return 6371.0 * haversine_distances([self_radians, other_radians])[0][1]
|
return distance / 1000.0 * 0.539957
|
||||||
|
|
||||||
|
def bearing(self, other):
|
||||||
|
geodesic = pyproj.Geod(ellps='WGS84')
|
||||||
|
forward, _, _ = geodesic.inv(self.Coordinate[1], self.Coordinate[0], other.Coordinate[1], other.Coordinate[0])
|
||||||
|
return forward
|
||||||
|
|
||||||
|
def project(self, bearing, distance):
|
||||||
|
geodesic = pyproj.Geod(ellps='WGS84')
|
||||||
|
longitude, latitude, _ = geodesic.fwd(self.Coordinate[1], self.Coordinate[0], bearing, distance)
|
||||||
|
return np.array([ latitude, longitude ])
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
# Recat departure separation in seconds
|
|
||||||
# x = CAT A -> CAT F
|
|
||||||
# y = CAT A -> CAT F
|
|
||||||
# https://www.skybrary.aero/index.php/RECAT_-_Wake_Turbulence_Re-categorisation
|
|
||||||
recatDeparture = [
|
|
||||||
[0, 100, 120, 140, 160, 180],
|
|
||||||
[0, 0, 0, 100, 120, 140],
|
|
||||||
[0, 0, 0, 80, 100, 120],
|
|
||||||
[0, 0, 0, 0, 0, 120],
|
|
||||||
[0, 0, 0, 0, 0, 100],
|
|
||||||
[0, 0, 0, 0, 0, 80],
|
|
||||||
]
|
|
||||||
|
|
||||||
#Recat Arrival in NM
|
|
||||||
recatArrival = [
|
|
||||||
[3, 4, 5, 5, 6, 8],
|
|
||||||
[0, 3, 4, 4, 5, 7],
|
|
||||||
[0, 0, 3, 3, 4, 6],
|
|
||||||
[0, 0, 0, 0, 0, 5],
|
|
||||||
[0, 0, 0, 0, 0, 4],
|
|
||||||
[0, 0, 0, 0, 0, 3],
|
|
||||||
]
|
|
||||||
10
main.py
10
main.py
@@ -1,10 +0,0 @@
|
|||||||
from tcp.TCPServer import TCPServer
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
server = TCPServer()
|
|
||||||
server.run()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
9
setup.py
9
setup.py
@@ -63,6 +63,7 @@ class build_py(_build_py):
|
|||||||
generateProtobuf('src/protobuf/AircraftReport.proto')
|
generateProtobuf('src/protobuf/AircraftReport.proto')
|
||||||
generateProtobuf('src/protobuf/AircraftSchedule.proto')
|
generateProtobuf('src/protobuf/AircraftSchedule.proto')
|
||||||
generateProtobuf('src/protobuf/BaseTypes.proto')
|
generateProtobuf('src/protobuf/BaseTypes.proto')
|
||||||
|
generateProtobuf('src/protobuf/Communication.proto')
|
||||||
_build_py.run(self)
|
_build_py.run(self)
|
||||||
|
|
||||||
with open('README.md', 'r') as f:
|
with open('README.md', 'r') as f:
|
||||||
@@ -77,13 +78,14 @@ setup(
|
|||||||
'aman.config',
|
'aman.config',
|
||||||
'aman.formats',
|
'aman.formats',
|
||||||
'aman.sys',
|
'aman.sys',
|
||||||
|
'aman.sys.aco',
|
||||||
'aman.tools',
|
'aman.tools',
|
||||||
'aman.types'
|
'aman.types'
|
||||||
],
|
],
|
||||||
namespace_packages = [ 'aman' ],
|
namespace_packages = [ 'aman' ],
|
||||||
description = 'AMAN optimization backend',
|
description = 'AMAN optimization backend',
|
||||||
long_description = longDescription,
|
long_description = longDescription,
|
||||||
author = 'Sven Czarnian',
|
author = 'Sven Czarnian, Pascal Seeler',
|
||||||
author_email = 'devel@svcz.de',
|
author_email = 'devel@svcz.de',
|
||||||
license = 'GPLv3',
|
license = 'GPLv3',
|
||||||
cmdclass = { 'clean': clean, 'build_py': build_py },
|
cmdclass = { 'clean': clean, 'build_py': build_py },
|
||||||
@@ -94,8 +96,9 @@ setup(
|
|||||||
'numpy',
|
'numpy',
|
||||||
'protobuf',
|
'protobuf',
|
||||||
'pyzmq',
|
'pyzmq',
|
||||||
'scikit-learn',
|
|
||||||
'scipy',
|
'scipy',
|
||||||
'setuptools'
|
'setuptools',
|
||||||
|
'flask',
|
||||||
|
'flask-cors'
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
Submodule src/protobuf updated: 0c5ed87078...893e012b3f
Reference in New Issue
Block a user