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"]
|
||||
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
|
||||
|
||||
87
aman/AMAN.py
87
aman/AMAN.py
@@ -2,7 +2,10 @@
|
||||
|
||||
import glob
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
from threading import Lock
|
||||
import time
|
||||
|
||||
from aman.com import AircraftReport_pb2
|
||||
from aman.com.Euroscope import Euroscope
|
||||
@@ -27,33 +30,32 @@ class AMAN:
|
||||
|
||||
def __init__(self):
|
||||
# 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()
|
||||
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
|
||||
self.systemConfig = System(os.path.join(configPath, 'System.ini'))
|
||||
self.SystemConfig = System(os.path.join(configPath, 'System.ini'))
|
||||
print('Parsed System.ini')
|
||||
|
||||
# read the aircraft performance data
|
||||
self.aircraftPerformance = AircraftPerformance(os.path.join(configPath, 'PerformanceData.ini'))
|
||||
if None == self.aircraftPerformance:
|
||||
self.AircraftPerformance = AircraftPerformance(os.path.join(configPath, 'PerformanceData.ini'))
|
||||
if None == self.AircraftPerformance:
|
||||
sys.stderr.write('No aircraft performance data found!')
|
||||
sys.exit(-1)
|
||||
else:
|
||||
print('Parsed PerformanceData.ini. Extracted ' + str(len(self.aircraftPerformance.aircrafts)) + ' aircrafts')
|
||||
print('Parsed PerformanceData.ini. Extracted ' + str(len(self.AircraftPerformance.Aircrafts)) + ' aircrafts')
|
||||
|
||||
self.weather = Weather()
|
||||
self.weather.acquire(self.systemConfig.Weather)
|
||||
# create the communication syb
|
||||
self.Weather = Weather(self.SystemConfig.Weather)
|
||||
self.Receiver = Euroscope(configPath, self.SystemConfig.Server, self)
|
||||
|
||||
self.acquireLock()
|
||||
|
||||
# find the airport configurations and create the workers
|
||||
airportsPath = os.path.join(os.path.join(configPath, 'airports'), '*.ini')
|
||||
@@ -64,34 +66,45 @@ class AMAN:
|
||||
airportConfig = Airport(file, icao)
|
||||
|
||||
# initialize the worker thread
|
||||
worker = Worker()
|
||||
worker.acquire(icao, airportConfig)
|
||||
self.workers.append(worker)
|
||||
worker = Worker(icao, airportConfig, self.Weather, self.AircraftPerformance, self.Receiver)
|
||||
self.Workers.append(worker)
|
||||
print('Started worker for ' + icao)
|
||||
|
||||
# create the EuroScope receiver
|
||||
self.receiver = Euroscope()
|
||||
self.receiver.acquire(configPath, self.systemConfig.Server, self)
|
||||
self.releaseLock()
|
||||
|
||||
def release(self):
|
||||
if None != self.receiver:
|
||||
self.receiver.release()
|
||||
self.receiver = None
|
||||
# initialize the random number generator
|
||||
random.seed(time.time())
|
||||
|
||||
if None != self.weather:
|
||||
self.weather.release()
|
||||
self.weather = None
|
||||
def acquireLock(self):
|
||||
if None != self.WorkersLock:
|
||||
self.WorkersLock.acquire()
|
||||
|
||||
if None != self.workers:
|
||||
for worker in self.workers:
|
||||
worker.release()
|
||||
self.workers = None
|
||||
def releaseLock(self):
|
||||
if None != self.WorkersLock:
|
||||
self.WorkersLock.release()
|
||||
|
||||
def updateAircraftReport(self, report : AircraftReport_pb2.AircraftReport):
|
||||
self.acquireLock()
|
||||
|
||||
# find the correct worker for the inbound
|
||||
for worker in self.workers:
|
||||
if worker.icao == report.destination:
|
||||
for worker in self.Workers:
|
||||
if worker.Icao == report.destination:
|
||||
worker.acquireLock()
|
||||
worker.reportQueue[report.aircraft.callsign] = report
|
||||
worker.ReportQueue[report.aircraft.callsign] = report
|
||||
worker.releaseLock()
|
||||
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)
|
||||
class DwdCrawler():
|
||||
def __init__(self):
|
||||
self.updateTime = None
|
||||
self.windData = None
|
||||
self.UpdateTime = None
|
||||
self.WindData = None
|
||||
|
||||
def parseGaforAreas(areas : str):
|
||||
areas = areas.replace(':', '')
|
||||
@@ -57,6 +57,9 @@ class DwdCrawler():
|
||||
altitude = int(altitude.replace('FL', '')) * 100
|
||||
else:
|
||||
altitude = int(altitude.replace('FT', ''))
|
||||
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)
|
||||
|
||||
@@ -107,7 +110,8 @@ class DwdCrawler():
|
||||
for line in content.splitlines():
|
||||
if '' == line:
|
||||
if 0 != len(windTable):
|
||||
windInformation.append(( areaIds, windTable ))
|
||||
for id in areaIds:
|
||||
windInformation.append([ id, windTable ])
|
||||
areaIds = None
|
||||
windTable = []
|
||||
elif line.startswith('GAFOR-Gebiete'):
|
||||
@@ -125,8 +129,8 @@ class DwdCrawler():
|
||||
return nextUpdate, windInformation
|
||||
|
||||
def receiveWindData(self):
|
||||
self.updateTime = None
|
||||
self.windData = None
|
||||
self.UpdateTime = None
|
||||
self.WindData = None
|
||||
|
||||
with urllib.request.urlopen('https://www.dwd.de/DE/fachnutzer/luftfahrt/teaser/luftsportberichte/luftsportberichte_node.html') as site:
|
||||
data = site.read().decode('utf-8')
|
||||
@@ -141,17 +145,18 @@ class DwdCrawler():
|
||||
pages.append('https://www.dwd.de/' + link['href'].split(';')[0])
|
||||
|
||||
# receive the wind data
|
||||
self.updateTime = None
|
||||
self.windData = []
|
||||
self.UpdateTime = None
|
||||
self.WindData = {}
|
||||
for page in pages:
|
||||
next, wind = self.parseGaforPage(page)
|
||||
if None != next:
|
||||
if None == self.updateTime or self.updateTime > next:
|
||||
self.updateTime = next
|
||||
self.windData.extend(wind)
|
||||
if None == self.UpdateTime or self.UpdateTime > next:
|
||||
self.UpdateTime = next
|
||||
for gafor in wind:
|
||||
self.WindData[gafor[0]] = gafor[1]
|
||||
|
||||
# indicate that new wind data is available
|
||||
if None != self.updateTime:
|
||||
if None != self.UpdateTime:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import ctypes
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
@@ -9,63 +8,49 @@ import time
|
||||
import zmq
|
||||
import zmq.auth
|
||||
|
||||
from aman.com import AircraftReport_pb2
|
||||
from aman.com import Communication_pb2
|
||||
from aman.config.Server import Server
|
||||
from threading import Thread, _active
|
||||
from threading import Thread
|
||||
|
||||
class ReceiverThread(Thread):
|
||||
def __init__(self, socket, aman):
|
||||
class ComThread(Thread):
|
||||
def __init__(self, com, aman):
|
||||
Thread.__init__(self)
|
||||
self.socket = socket
|
||||
self.aman = aman
|
||||
self.Com = com
|
||||
self.AMAN = aman
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
try:
|
||||
msg = self.socket.recv(zmq.NOBLOCK)
|
||||
msg = self.Com.Socket.recv(zmq.NOBLOCK)
|
||||
|
||||
# parse the received message
|
||||
report = AircraftReport_pb2.AircraftReport()
|
||||
report = Communication_pb2.AircraftUpdate()
|
||||
report.ParseFromString(msg)
|
||||
|
||||
# try to associate the received aircraft to an airport
|
||||
self.aman.updateAircraftReport(report)
|
||||
# try to associate the received aircrafts to airports
|
||||
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:
|
||||
if zmq.EAGAIN == error.errno:
|
||||
time.sleep(0.5)
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
else:
|
||||
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
|
||||
class Euroscope:
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.receiverSocket = None
|
||||
self.receiverThread = None
|
||||
self.notificationSocket = None
|
||||
|
||||
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()
|
||||
def __init__(self, configPath : str, config : Server, aman):
|
||||
self.Context = None
|
||||
self.Socket = None
|
||||
self.Thread = None
|
||||
self.Context = zmq.Context()
|
||||
|
||||
# find the key directories
|
||||
serverKeyPath = os.path.join(os.path.join(configPath, 'keys'), 'server')
|
||||
@@ -88,34 +73,62 @@ class Euroscope:
|
||||
keyPair = zmq.auth.load_certificate(keyPairPath[0])
|
||||
|
||||
# initialize the receiver
|
||||
self.receiverSocket = zmq.Socket(self.context, zmq.SUB)
|
||||
self.receiverSocket.setsockopt(zmq.CURVE_PUBLICKEY, keyPair[0])
|
||||
self.receiverSocket.setsockopt(zmq.CURVE_SECRETKEY, keyPair[1])
|
||||
self.receiverSocket.setsockopt(zmq.CURVE_SERVER, True)
|
||||
self.receiverSocket.bind('tcp://' + config.Address + ':' + str(config.PortReceiver))
|
||||
self.receiverSocket.setsockopt(zmq.SUBSCRIBE, b'')
|
||||
self.receiverThread = ReceiverThread(self.receiverSocket, aman)
|
||||
self.receiverThread.start()
|
||||
self.Socket = zmq.Socket(self.Context, zmq.REP)
|
||||
self.Socket.setsockopt(zmq.CURVE_PUBLICKEY, keyPair[0])
|
||||
self.Socket.setsockopt(zmq.CURVE_SECRETKEY, keyPair[1])
|
||||
self.Socket.setsockopt(zmq.CURVE_SERVER, True)
|
||||
self.Socket.bind('tcp://' + config.Address + ':' + str(config.PortReceiver))
|
||||
#self.Socket.setsockopt(zmq.SUBSCRIBE, b'')
|
||||
self.Thread = ComThread(self, aman)
|
||||
self.Thread.setDaemon(True)
|
||||
self.Thread.start()
|
||||
print('Listening to tcp://' + config.Address + ':' + str(config.PortReceiver))
|
||||
|
||||
# initialize the notification
|
||||
self.notificationSocket = zmq.Socket(self.context, zmq.PUB)
|
||||
self.notificationSocket.setsockopt(zmq.CURVE_PUBLICKEY, keyPair[0])
|
||||
self.notificationSocket.setsockopt(zmq.CURVE_SECRETKEY, keyPair[1])
|
||||
self.notificationSocket.setsockopt(zmq.CURVE_SERVER, True)
|
||||
self.notificationSocket.bind('tcp://' + config.Address + ':' + str(config.PortNotification))
|
||||
print('Publishing to tcp://' + config.Address + ':' + str(config.PortNotification))
|
||||
def sendSequence(self, airport : str, inbounds, weather):
|
||||
if None == self.Socket:
|
||||
return
|
||||
|
||||
def release(self):
|
||||
if None != self.receiverThread:
|
||||
self.receiverThread.stopThread()
|
||||
self.receiverThread.join()
|
||||
self.receiverThread = None
|
||||
sequence = Communication_pb2.AircraftSequence()
|
||||
sequence.airport = airport
|
||||
|
||||
if None != self.receiverSocket:
|
||||
self.receiverSocket.close()
|
||||
self.receiverSocket = None
|
||||
# convert the wind data
|
||||
if None != weather.Altitudes:
|
||||
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:
|
||||
self.notificationSocket.close()
|
||||
self.notificationSocket = None
|
||||
# convert the inbound sequence
|
||||
for inbound in inbounds:
|
||||
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
|
||||
|
||||
class Weather(Thread):
|
||||
def __init__(self):
|
||||
def __init__(self, config : aman.config.Weather.Weather):
|
||||
Thread.__init__(self)
|
||||
|
||||
self.nextUpdate = None
|
||||
self.lastUpdateTried = None
|
||||
self.stopThread = False
|
||||
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
|
||||
self.NextUpdate = dt.utcfromtimestamp(int(time.time()))
|
||||
self.LastUpdateTried = None
|
||||
self.StopThread = False
|
||||
self.Provider = None
|
||||
|
||||
if 'DWD' == config.Provider.upper():
|
||||
self.provider = DwdCrawler()
|
||||
else:
|
||||
self.Provider = DwdCrawler()
|
||||
elif 'NONE' != config.Provider.upper():
|
||||
sys.stderr.write('Invalid or unknown weather-provider defined')
|
||||
sys.exit(-1)
|
||||
|
||||
self.setDaemon(True)
|
||||
self.start()
|
||||
|
||||
def release(self):
|
||||
self.stopThread = True
|
||||
self.join()
|
||||
|
||||
def currentClock():
|
||||
clock = dt.utcfromtimestamp(int(time.time())).replace(tzinfo = pytz.UTC)
|
||||
return clock
|
||||
|
||||
def run(self):
|
||||
while False == self.stopThread and None != self.provider:
|
||||
while False == self.StopThread and None != self.Provider:
|
||||
now = Weather.currentClock()
|
||||
|
||||
# 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)
|
||||
continue
|
||||
|
||||
if None == self.lastUpdateTried or self.lastUpdateTried <= now:
|
||||
if True == self.provider.receiveWindData():
|
||||
self.nextUpdate = self.provider.updateTime
|
||||
if None == self.LastUpdateTried or self.LastUpdateTried <= now:
|
||||
if True == self.Provider.receiveWindData():
|
||||
self.NextUpdate = self.Provider.UpdateTime
|
||||
print('Received new wind data')
|
||||
@@ -8,7 +8,7 @@ class AircraftPerformance:
|
||||
def __init__(self, filepath : str):
|
||||
config = configparser.ConfigParser()
|
||||
config.read(filepath)
|
||||
self.aircrafts = { }
|
||||
self.Aircrafts = { }
|
||||
|
||||
# iterate over all entries
|
||||
for key in config:
|
||||
@@ -17,12 +17,12 @@ class AircraftPerformance:
|
||||
|
||||
aircraft = PerformanceData(key)
|
||||
|
||||
aircraft.speedAboveFL240 = config[key]['speedabovefl240']
|
||||
aircraft.rodAboveFL240 = config[key]['rodabovefl240']
|
||||
aircraft.speedAboveFL100 = config[key]['speedabovefl100']
|
||||
aircraft.rodAboveFL100 = config[key]['rodabovefl100']
|
||||
aircraft.speedBelowFL100 = config[key]['speedbelowfl100']
|
||||
aircraft.rodBelowFL100 = config[key]['rodbelowfl100']
|
||||
aircraft.speedApproach = config[key]['speedapproach']
|
||||
aircraft.SpeedAboveFL240 = float(config[key]['speedabovefl240'])
|
||||
aircraft.RodAboveFL240 = float(config[key]['rodabovefl240'])
|
||||
aircraft.SpeedAboveFL100 = float(config[key]['speedabovefl100'])
|
||||
aircraft.RodAboveFL100 = float(config[key]['rodabovefl100'])
|
||||
aircraft.SpeedBelowFL100 = float(config[key]['speedbelowfl100'])
|
||||
aircraft.RodBelowFL100 = float(config[key]['rodbelowfl100'])
|
||||
aircraft.SpeedApproach = float(config[key]['speedapproach'])
|
||||
|
||||
self.aircrafts[aircraft.icao] = aircraft
|
||||
self.Aircrafts[aircraft.Icao] = aircraft
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import configparser
|
||||
from datetime import timedelta
|
||||
import glob
|
||||
import os
|
||||
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:
|
||||
def findGngData(data, path):
|
||||
@@ -30,14 +35,282 @@ class Airport:
|
||||
return []
|
||||
return planning['routes'].split(':')
|
||||
|
||||
def __init__(self, filepath : str, icao : str):
|
||||
self.arrivalRoutes = {}
|
||||
def parseDefaultSequencingConfiguration(self, icao : str, planning):
|
||||
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.read(filepath)
|
||||
|
||||
dataConfig = None
|
||||
planningConfig = None
|
||||
rhcConfig = None
|
||||
webUiConfig = None
|
||||
|
||||
# search the required sections
|
||||
for key in config:
|
||||
@@ -45,6 +318,10 @@ class Airport:
|
||||
dataConfig = config['DATA']
|
||||
elif 'PLANNING' == key:
|
||||
planningConfig = config['PLANNING']
|
||||
elif 'RHC' == key:
|
||||
rhcConfig = config['RHC']
|
||||
elif 'WEBUI' == key:
|
||||
webUiConfig = config['WEBUI']
|
||||
|
||||
# find the GNG-file data
|
||||
sctFile, eseFile = Airport.findGngData(dataConfig, os.path.dirname(filepath))
|
||||
@@ -61,6 +338,40 @@ class Airport:
|
||||
sys.stderr.write('No valid planning configuration found')
|
||||
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
|
||||
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.PortReceiver = None
|
||||
self.PortNotification = None
|
||||
self.WebUiUrl = None
|
||||
self.WebUiSequenceNotification = None
|
||||
self.WebUiConfigurationReceiver = None
|
||||
|
||||
# search the required sections
|
||||
for key in config:
|
||||
|
||||
@@ -6,8 +6,6 @@ import sys
|
||||
class Weather():
|
||||
def __init__(self, config : configparser.ConfigParser):
|
||||
self.Provider = None
|
||||
self.PortReceiver = None
|
||||
self.PortNotification = None
|
||||
|
||||
# search the required sections
|
||||
for key in config:
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import copy
|
||||
import sys
|
||||
|
||||
from aman.types.ArrivalRoute import ArrivalRoute
|
||||
from aman.types.Runway import Runway
|
||||
from aman.types.Waypoint import Waypoint
|
||||
|
||||
class SctEseFormat:
|
||||
@@ -30,11 +32,22 @@ class SctEseFormat:
|
||||
if len(split) <= longitudeIdx:
|
||||
sys.stderr.write('Invalid waypoint format: ' + waypoint)
|
||||
sys.exit(-1)
|
||||
return Waypoint(split[nameIdx], Waypoint.dms2dd(split[latitudeIdx]), Waypoint.dms2dd(split[longitudeIdx]))
|
||||
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)
|
||||
foundAirports = False
|
||||
foundRunways = False
|
||||
foundVOR = False
|
||||
foundNDB = False
|
||||
foundFix = False
|
||||
@@ -48,6 +61,8 @@ class SctEseFormat:
|
||||
foundFix = True
|
||||
elif 'AIRPORT' == key:
|
||||
foundAirports = True
|
||||
elif 'RUNWAY' == key:
|
||||
foundRunways = True
|
||||
|
||||
if False == foundVOR:
|
||||
sys.stderr.write('Unable to find VOR-entries in the sector file')
|
||||
@@ -61,27 +76,39 @@ class SctEseFormat:
|
||||
if False == foundAirports:
|
||||
sys.stderr.write('Unable to find AIRPORT-entries in the sector file')
|
||||
sys.exit(-1)
|
||||
if False == foundRunways:
|
||||
sys.stderr.write('Unable to find RUNWAY-entries in the sector file')
|
||||
sys.exit(-1)
|
||||
|
||||
# extract all waypoints
|
||||
for waypoint in config['VOR']:
|
||||
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 2, 3)
|
||||
self.waypoints.setdefault(waypoint.name, []).append(waypoint)
|
||||
self.Waypoints.setdefault(waypoint.Name, []).append(waypoint)
|
||||
for waypoint in config['NDB']:
|
||||
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 1, 2)
|
||||
self.waypoints.setdefault(waypoint.name, []).append(waypoint)
|
||||
self.Waypoints.setdefault(waypoint.Name, []).append(waypoint)
|
||||
for waypoint in config['FIXES']:
|
||||
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 1, 2)
|
||||
self.waypoints.setdefault(waypoint.name, []).append(waypoint)
|
||||
self.Waypoints.setdefault(waypoint.Name, []).append(waypoint)
|
||||
|
||||
# extract the airports
|
||||
for airport in config['AIRPORT']:
|
||||
airport = SctEseFormat.parseWaypoint(airport,0, 2, 3)
|
||||
self.airports.setdefault(airport.name, []).append(airport)
|
||||
airport = SctEseFormat.parseWaypoint(airport, 0, 2, 3)
|
||||
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):
|
||||
# split the route and validate that it is a STAR for the airport
|
||||
split = route.split(':')
|
||||
if 5 != len(split) or 'STAR' != split[0] or split[1] != airport.name:
|
||||
if 5 != len(split) or 'STAR' != split[0] or split[1] != airport.Name:
|
||||
return None
|
||||
|
||||
# find all waypoints
|
||||
@@ -89,7 +116,7 @@ class SctEseFormat:
|
||||
route = list(filter(None, split[4].split(' ')))
|
||||
for waypoint in route:
|
||||
# find the waypoint in the route
|
||||
coordinates = self.waypoints[waypoint]
|
||||
coordinates = self.Waypoints[waypoint]
|
||||
# no waypoint with this name defined
|
||||
if None == coordinates:
|
||||
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.exit(-1)
|
||||
|
||||
waypoints.append(nearest)
|
||||
waypoints.append(copy.deepcopy(nearest))
|
||||
# extend the list of waypoints
|
||||
else:
|
||||
waypoints.append(coordinates[0])
|
||||
waypoints.append(copy.deepcopy(coordinates[0]))
|
||||
|
||||
# create the arrival route
|
||||
return ArrivalRoute(split[3], split[2], waypoints)
|
||||
@@ -124,10 +151,10 @@ class SctEseFormat:
|
||||
foundSidsStars = False
|
||||
|
||||
# search the airport in the extracted list
|
||||
if not airport in self.airports:
|
||||
sys.stderr.write(airport + 'in self.airports', 'Unable to find the requested airport')
|
||||
if not airport in self.Airports:
|
||||
sys.stderr.write('Unable to find the requested airport')
|
||||
sys.exit(-1)
|
||||
airport = self.airports[airport][0]
|
||||
airport = self.Airports[airport][0]
|
||||
|
||||
for key in config:
|
||||
if 'SIDSSTARS' == key:
|
||||
@@ -140,13 +167,14 @@ class SctEseFormat:
|
||||
# parse all arrival routes
|
||||
for line in config['SIDSSTARS']:
|
||||
route = self.parseArrivalRoute(line, airport)
|
||||
if None != route and route.name in allowedRoutes:
|
||||
self.arrivalRoutes.setdefault(route.runway, []).append(route)
|
||||
if None != route and route.Name in allowedRoutes:
|
||||
self.ArrivalRoutes.setdefault(route.Runway, []).append(route)
|
||||
|
||||
def __init__(self, sctFilepath : str, eseFilepath : str, airport : str, allowedRoutes : list):
|
||||
self.arrivalRoutes = {}
|
||||
self.waypoints = {}
|
||||
self.airports = {}
|
||||
self.ArrivalRoutes = {}
|
||||
self.Waypoints = {}
|
||||
self.Airports = {}
|
||||
self.Runways = {}
|
||||
|
||||
self.extractWaypoints(sctFilepath)
|
||||
self.extractSctInformation(sctFilepath)
|
||||
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:
|
||||
def __init__(self, gaforId, weather : Weather):
|
||||
self.gafor = gaforId
|
||||
self.weather = weather
|
||||
self.windDirectionModel = None
|
||||
self.windSpeedModel = None
|
||||
self.lastWeatherUpdate = None
|
||||
self.minimumAltitude = 1000000
|
||||
self.maximumAltitude = -1
|
||||
self.Gafor = gaforId
|
||||
self.Weather = weather
|
||||
self.Altitudes = None
|
||||
self.Directions = None
|
||||
self.Windspeeds = None
|
||||
self.WindDirectionModel = None
|
||||
self.WindSpeedModel = None
|
||||
self.LastWeatherUpdate = None
|
||||
self.MinimumAltitude = 1000000
|
||||
self.MaximumAltitude = -1
|
||||
|
||||
# create the density interpolation model
|
||||
# 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())
|
||||
|
||||
def updateWindModel(self):
|
||||
if None == self.lastWeatherUpdate or self.lastWeatherUpdate != self.weather.provider.updateTime:
|
||||
self.lastWeatherUpdate = self.weather.provider.updateTime
|
||||
if None == self.Weather or None == self.Weather.Provider:
|
||||
return
|
||||
|
||||
self.minimumAltitude = 1000000
|
||||
self.maximumAltitude = -1
|
||||
self.windDirectionModel = None
|
||||
self.windSpeedModel = None
|
||||
if None == self.LastWeatherUpdate or self.LastWeatherUpdate != self.Weather.Provider.UpdateTime:
|
||||
self.MinimumAltitude = 1000000
|
||||
self.MaximumAltitude = -1
|
||||
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:
|
||||
altitudes = []
|
||||
directions = []
|
||||
speeds = []
|
||||
if None != self.Weather.Provider.WindData and self.Gafor in self.Weather.Provider.WindData:
|
||||
self.Altitudes = []
|
||||
self.Directions = []
|
||||
self.Windspeeds = []
|
||||
|
||||
# collect the data for the wind model
|
||||
for level in self.weather.provider.windData[self.gafor]:
|
||||
altitudes.append(level[0])
|
||||
directions.append(level[1])
|
||||
speeds.append(level[2])
|
||||
for level in self.Weather.Provider.WindData[self.Gafor]:
|
||||
self.Altitudes.append(level[0])
|
||||
self.Directions.append(level[1])
|
||||
self.Windspeeds.append(level[2])
|
||||
|
||||
# define the thresholds for later boundary checks
|
||||
if self.minimumAltitude > level[0]:
|
||||
self.minimumAltitude = level[0]
|
||||
if self.maximumAltitude < level[0]:
|
||||
self.maximumAltitude = level[0]
|
||||
if self.MinimumAltitude > level[0]:
|
||||
self.MinimumAltitude = level[0]
|
||||
if self.MaximumAltitude < level[0]:
|
||||
self.MaximumAltitude = level[0]
|
||||
|
||||
# calculate the models
|
||||
if 1 < len(altitudes):
|
||||
self.windDirectionModel = scipy.interpolate.interp1d(altitudes, directions)
|
||||
self.windSpeedModel = scipy.interpolate.interp1d(altitudes, speeds)
|
||||
if 1 < len(self.Altitudes):
|
||||
self.WindDirectionModel = scipy.interpolate.interp1d(self.Altitudes, self.Directions)
|
||||
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()
|
||||
tas = self.calculateTAS(altitude, ias)
|
||||
|
||||
# initialize the wind data
|
||||
if None != self.windDirectionModel and None != self.windSpeedModel:
|
||||
# initialized the wind data
|
||||
if None != self.WindDirectionModel and None != self.WindSpeedModel:
|
||||
direction = 0.0
|
||||
speed = 0.0
|
||||
if None != self.windSpeedModel and None != self.windDirectionModel:
|
||||
if self.maximumAltitude <= altitude:
|
||||
altitude = self.maximumAltitude - 1
|
||||
if self.minimumAltitude >= altitude:
|
||||
altitude = self.minimumAltitude + 1
|
||||
direction = self.windDirectionModel(altitude).item()
|
||||
speed = self.windSpeedModel(altitude).item()
|
||||
if None != self.WindSpeedModel and None != self.WindDirectionModel:
|
||||
if self.MaximumAltitude <= altitude:
|
||||
altitude = self.MaximumAltitude - 1
|
||||
if self.MinimumAltitude >= altitude:
|
||||
altitude = self.MinimumAltitude + 1
|
||||
direction = self.WindDirectionModel(altitude).item()
|
||||
speed = self.WindSpeedModel(altitude).item()
|
||||
else:
|
||||
speed = 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))
|
||||
|
||||
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
|
||||
|
||||
from threading import Thread, Lock
|
||||
import sys
|
||||
import time
|
||||
|
||||
from aman.com import Weather
|
||||
from aman.com.Euroscope import Euroscope
|
||||
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):
|
||||
def __init__(self):
|
||||
def __init__(self, icao : str, configuration : Airport, weather : Weather,
|
||||
performance : PerformanceData, euroscope : Euroscope):
|
||||
Thread.__init__(self)
|
||||
self.stopThread = None
|
||||
self.icao = None
|
||||
self.configuration = None
|
||||
self.arrivalRoutes = None
|
||||
self.updateLock = None
|
||||
self.reportQueue = {}
|
||||
self.StopThread = None
|
||||
self.Icao = icao
|
||||
self.Configuration = configuration
|
||||
self.SequencingConfiguration = configuration.DefaultSequencing
|
||||
self.PerformanceData = performance
|
||||
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):
|
||||
self.release()
|
||||
# merge the constraint information with the GNG information
|
||||
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):
|
||||
self.stopThread = None
|
||||
self.icao = icao
|
||||
self.configuration = configuration
|
||||
self.arrivalRoutes = configuration.gngData.arrivalRoutes
|
||||
self.updateLock = Lock()
|
||||
self.reportQueue = {}
|
||||
for waypoint in star.Route:
|
||||
if constraint.Name == waypoint.Name:
|
||||
waypoint.Altitude = constraint.Altitude
|
||||
waypoint.Speed = constraint.Speed
|
||||
waypoint.BaseTurn = constraint.BaseTurn
|
||||
waypoint.FinalTurn = constraint.FinalTurn
|
||||
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()
|
||||
|
||||
def acquireLock(self):
|
||||
if None != self.updateLock:
|
||||
self.updateLock.acquire()
|
||||
|
||||
def release(self):
|
||||
self.stopThread = True
|
||||
self.join()
|
||||
if None != self.UpdateLock:
|
||||
self.UpdateLock.acquire()
|
||||
|
||||
def releaseLock(self):
|
||||
if None != self.updateLock:
|
||||
self.updateLock.release()
|
||||
if None != self.UpdateLock:
|
||||
self.UpdateLock.release()
|
||||
|
||||
def run(self):
|
||||
counter = 0
|
||||
|
||||
while None == self.stopThread:
|
||||
while None == self.StopThread:
|
||||
time.sleep(1)
|
||||
counter += 1
|
||||
if 0 != (counter % 60):
|
||||
continue
|
||||
|
||||
# TODO handle the report queue and update internal information
|
||||
# TODO execute planning, etc.
|
||||
continue
|
||||
self.acquireLock()
|
||||
|
||||
# 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
|
||||
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
import os
|
||||
import sys
|
||||
from typing import Tuple
|
||||
import zmq.auth
|
||||
|
||||
@@ -10,14 +12,12 @@ import zmq.auth
|
||||
# @return The public and private key tuple
|
||||
def KeyPairCreator(directory: str, server: bool) -> Tuple[str, str]:
|
||||
if not server:
|
||||
print('Creating a new pair for a client...')
|
||||
target = 'client'
|
||||
else:
|
||||
print('Creating a new pair for the server...')
|
||||
target = 'server'
|
||||
|
||||
public, private = zmq.auth.create_certificates(directory, target)
|
||||
return (public, private)
|
||||
return public, private
|
||||
|
||||
def str2bool(value):
|
||||
if isinstance(value, bool):
|
||||
@@ -29,16 +29,71 @@ def str2bool(value):
|
||||
else:
|
||||
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__':
|
||||
# create the commandline parser
|
||||
parser = argparse.ArgumentParser(description='Create a new key-value pair')
|
||||
parser.add_argument('directory', help='Directory where to store the key pair')
|
||||
parser.add_argument('--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")
|
||||
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
|
||||
if not os.path.exists(args.directory):
|
||||
os.makedirs(args.directory)
|
||||
|
||||
# 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')
|
||||
|
||||
aircrafts = []
|
||||
parsed = 0
|
||||
for link in links:
|
||||
valid, aircraft = parsePerformanceData(link)
|
||||
|
||||
parsed += 1
|
||||
print('Parsed ' + str(parsed) + ' of ' + str(len(links)), end='\r')
|
||||
|
||||
if False == valid:
|
||||
print('Unable to find performance data for ' + link)
|
||||
continue
|
||||
|
||||
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:
|
||||
def __init__(self, name : str, runway : str, waypoints : list):
|
||||
self.name = name
|
||||
self.runway = runway
|
||||
self.iaf = waypoints[0]
|
||||
self.route = waypoints
|
||||
self.Name = name
|
||||
self.Runway = runway
|
||||
self.Iaf = waypoints[0]
|
||||
self.Route = waypoints
|
||||
|
||||
def __str__(self):
|
||||
return 'Name: ' + self.name + ', IAF: ' + self.iaf.name + ', RWY: ' + self.runway
|
||||
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
|
||||
|
||||
import pytz
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from aman.com import AircraftReport_pb2
|
||||
from aman.sys.WeatherModel import WeatherModel
|
||||
from aman.types.PerformanceData import PerformanceData
|
||||
|
||||
class Inbound:
|
||||
def __init__(self, report : AircraftReport_pb2.AircraftReport):
|
||||
self.report = report
|
||||
def __init__(self, report : AircraftReport_pb2.AircraftReport, performanceData : PerformanceData):
|
||||
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:
|
||||
def __init__(self, icao : str):
|
||||
self.icao = icao
|
||||
self.speedAboveFL240 = 0.0
|
||||
self.speedAboveFL100 = 0.0
|
||||
self.speedBelowFL100 = 0.0
|
||||
self.speedApproach = 0.0
|
||||
self.rodAboveFL240 = 0.0
|
||||
self.rodAboveFL100 = 0.0
|
||||
self.rodBelowFL100 = 2000.0
|
||||
self.Icao = icao
|
||||
self.SpeedAboveFL240 = 0.0
|
||||
self.SpeedAboveFL100 = 0.0
|
||||
self.SpeedBelowFL100 = 0.0
|
||||
self.SpeedApproach = 0.0
|
||||
self.RodAboveFL240 = 0.0
|
||||
self.RodAboveFL100 = 0.0
|
||||
self.RodBelowFL100 = 2000.0
|
||||
|
||||
def 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):
|
||||
return 'ICAO: ' + self.icao + ', FL240: ' + str(self.rodAboveFL240) + '@' + str(self.speedAboveFL240) + \
|
||||
', +FL100: ' + str(self.rodAboveFL100) + '@' + str(self.speedAboveFL100) + \
|
||||
', -FL100: ' + str(self.rodBelowFL100) + '@' + str(self.speedBelowFL100) + \
|
||||
', Vapp: ' + str(self.speedApproach)
|
||||
return 'ICAO: ' + self.icao + ', FL240: ' + str(self.RodAboveFL240) + '@' + str(self.SpeedAboveFL240) + \
|
||||
', +FL100: ' + str(self.RodAboveFL100) + '@' + str(self.SpeedAboveFL100) + \
|
||||
', -FL100: ' + str(self.RodBelowFL100) + '@' + str(self.SpeedBelowFL100) + \
|
||||
', Vapp: ' + str(self.SpeedApproach)
|
||||
|
||||
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
|
||||
|
||||
from sklearn.metrics.pairwise import haversine_distances
|
||||
import numpy as np
|
||||
import pyproj
|
||||
|
||||
class Waypoint:
|
||||
def dms2dd(coordinate : str):
|
||||
@@ -20,14 +20,59 @@ class Waypoint:
|
||||
|
||||
return dd
|
||||
|
||||
def __init__(self, name : str, latitude : float, longitude : float):
|
||||
self.name = name
|
||||
self.coordinate = np.array([ latitude, longitude ])
|
||||
def coordinateArgument(value):
|
||||
if True == isinstance(value, str):
|
||||
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):
|
||||
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):
|
||||
self_radians = [np.radians(_) for _ in self.coordinate]
|
||||
other_radians = [np.radians(_) for _ in other.coordinate]
|
||||
return 6371.0 * haversine_distances([self_radians, other_radians])[0][1]
|
||||
geodesic = pyproj.Geod(ellps='WGS84')
|
||||
_, _, distance = geodesic.inv(self.Coordinate[1], self.Coordinate[0], other.Coordinate[1], other.Coordinate[0])
|
||||
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/AircraftSchedule.proto')
|
||||
generateProtobuf('src/protobuf/BaseTypes.proto')
|
||||
generateProtobuf('src/protobuf/Communication.proto')
|
||||
_build_py.run(self)
|
||||
|
||||
with open('README.md', 'r') as f:
|
||||
@@ -77,13 +78,14 @@ setup(
|
||||
'aman.config',
|
||||
'aman.formats',
|
||||
'aman.sys',
|
||||
'aman.sys.aco',
|
||||
'aman.tools',
|
||||
'aman.types'
|
||||
],
|
||||
namespace_packages = [ 'aman' ],
|
||||
description = 'AMAN optimization backend',
|
||||
long_description = longDescription,
|
||||
author = 'Sven Czarnian',
|
||||
author = 'Sven Czarnian, Pascal Seeler',
|
||||
author_email = 'devel@svcz.de',
|
||||
license = 'GPLv3',
|
||||
cmdclass = { 'clean': clean, 'build_py': build_py },
|
||||
@@ -94,8 +96,9 @@ setup(
|
||||
'numpy',
|
||||
'protobuf',
|
||||
'pyzmq',
|
||||
'scikit-learn',
|
||||
'scipy',
|
||||
'setuptools'
|
||||
'setuptools',
|
||||
'flask',
|
||||
'flask-cors'
|
||||
]
|
||||
)
|
||||
|
||||
Submodule src/protobuf updated: 0c5ed87078...893e012b3f
Reference in New Issue
Block a user