Compare commits
301 Commits
master
...
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 | ||
|
|
a1c48d7851 | ||
|
|
a86dfa01d8 | ||
|
|
c6d22d2067 | ||
|
|
23add20513 | ||
|
|
fd324ea747 | ||
|
|
ebea408267 | ||
|
|
8b43991c50 | ||
|
|
11eae85e35 | ||
|
|
7e17bf0103 | ||
|
|
667829b03d | ||
|
|
dc2a435e8e | ||
|
|
484be00e8c | ||
|
|
b69c584fb4 | ||
|
|
af52103ec8 | ||
|
|
11e76a3f24 | ||
|
|
b54f7dfc50 | ||
|
|
2687f543ad | ||
|
|
3743f31b84 | ||
|
|
d851efcd4d | ||
|
|
9fd05aa932 | ||
|
|
d07751cf77 | ||
|
|
715433bac6 | ||
|
|
9de9b813ba | ||
|
|
1561335e1b | ||
|
|
b516333ede | ||
|
|
793d92ff83 | ||
|
|
fa38924936 | ||
|
|
87d813d0a4 | ||
|
|
cf191a6ff1 | ||
|
|
aaa37a5f62 | ||
|
|
744ad71b6c | ||
|
|
479d7b2d44 | ||
|
|
64c238899a | ||
|
|
518e80e2fe | ||
|
|
46cc87eb3b | ||
|
|
bd7cbe91ed | ||
|
|
f6643d899f | ||
|
|
b10fae513e | ||
|
|
d60d5cb716 | ||
|
|
b3b5b3e547 | ||
|
|
9c7d8db006 | ||
|
|
8cd5aa6baf | ||
|
|
df455df689 | ||
|
|
1374ad95c9 | ||
|
|
31150adb9e | ||
|
|
e4715abda3 | ||
|
|
51b4013e6b | ||
|
|
f4fbd6245b | ||
|
|
1d50f0e9af | ||
|
|
8f81b65df8 | ||
|
|
c7738346bb | ||
|
|
4e8e8f15e4 | ||
|
|
5a2b9983b6 | ||
|
|
36d2bfa8a0 | ||
|
|
0fdcf8e99e | ||
|
|
6b2072f43b | ||
|
|
04b299730a | ||
|
|
8199b33d53 | ||
|
|
153930e73c | ||
|
|
7b26e27c9d | ||
|
|
a3f4f8f41b | ||
|
|
59e458c70b | ||
|
|
a0c9676c78 | ||
|
|
b92f437fcb | ||
|
|
355a5463e5 | ||
|
|
d4c07824c6 | ||
|
|
bbd45778db | ||
|
|
bd2d431c41 | ||
|
|
3076821b3a | ||
|
|
0f885c1e00 | ||
|
|
e112ee9694 | ||
|
|
7d540a9b85 | ||
|
|
30729676ac | ||
|
|
c80d230946 | ||
|
|
a0d4a1e0d3 |
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
Backend.ini
|
||||
main.py
|
||||
.vscode/
|
||||
*_pb2.py
|
||||
__pycache__
|
||||
*.egg-info
|
||||
build/
|
||||
dist/
|
||||
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[submodule "src/protobuf"]
|
||||
path = src/protobuf
|
||||
url = git@git.ascarion.org:vatger/aman-com.git
|
||||
branch = feature/protobuf
|
||||
69
README.md
69
README.md
@@ -1,2 +1,69 @@
|
||||
# aman-sys
|
||||
# Arrival MANanager (AMAN)
|
||||
|
||||
## System description
|
||||
|
||||
AMAN is splitted up into four different components.
|
||||
* aman-com defines the diffent message types
|
||||
* aman-es implements an EuroScope plugin to communicate with [aman-sys](https://git.vatsim-germany.org/nav/aman-sys)
|
||||
* aman-sys implements the backend system to plan an optimal arrival sequence for the different airports
|
||||
* aman-web implements a web-interface to configure [aman-sys](https://git.vatsim-germany.org/nav/aman-sys) and visualize sequences
|
||||
|
||||
## Component description
|
||||
|
||||
AMAN uses [Protocol Buffers](https://developers.google.com/protocol-buffers)
|
||||
for the message serialization and message definition between the EuroScope instance and the AMAN backend.
|
||||
|
||||
Additionally is [ZeroMQ](https://zeromq.org/) used for the communication abstraction layer.
|
||||
|
||||
This component provides the server backend with the planning and optimization system per airport.
|
||||
It is designed as a python framework that can run on a webserver.
|
||||
ZMQ based encryption and authentication methods are used to authenticate controllers.
|
||||
|
||||
## RHC-ACS-ASS Algorithm
|
||||
Step 1: Initialization. Set up parameters for
|
||||
the RHC, and set the current receding horizon k = 1.
|
||||
|
||||
Step 2: Find out all the M aircraft whose PLTs belong to
|
||||
the kth receding horizon.
|
||||
|
||||
Step 3: Schedule the M aircraft in the kth receding horizon
|
||||
by using an ACS.
|
||||
|
||||
Step 4: Assign the aircraft whose ALTs belong to kth scheduled window ω(k) to land on
|
||||
the runway.
|
||||
|
||||
Step 5: Modify the PLT for those aircraft whose PLT belongs to ω(k) but the ALT does not belong to ω(k). The modification is to set their PLT to kTTI, making them belong to Ω(k + 1), such that they can be scheduled in the next receding horizon.
|
||||
|
||||
Step 6: Termination check. When all the aircraft have been assigned to land at the runway, the algorithm terminates. Otherwise, set k = k + 1 and go to Step 2 for the next receding horizon optimization.
|
||||
|
||||
|
||||
|
||||
In the preceding steps, Step 3 is the major process of the
|
||||
algorithm. The flowchart is illustrated on the right side of Fig. 3,
|
||||
and the details are given below.
|
||||
|
||||
Step 3.1: Schedule the M aircraft by the FCFS approach and
|
||||
calculate the fitness value through (3). Calculate
|
||||
the initial pheromone τ0 and set the pheromone for
|
||||
each aircraft pair as τ0.
|
||||
|
||||
Step 3.2: For each ant, do the following.
|
||||
|
||||
a) Determine the first landing aircraft s and construct the whole landing sequence using the state transition rule as (5) and (6).
|
||||
|
||||
b) Perform the local pheromone updating as (9).
|
||||
|
||||
Step 3.3: Calculate the fitness of each ant and determine
|
||||
the best solution. Moreover, the current best solution is compared with the historically best solution
|
||||
to determine the historically best solution.
|
||||
|
||||
Step 3.4: Perform the global pheromone updating as (10).
|
||||
|
||||
# Additional libraries
|
||||
|
||||
* [ZeroMQ](https://github.com/zeromq) - GNU GPLv3
|
||||
* [Protocol Buffers](https://github.com/protocolbuffers/protobuf) - BSD-3
|
||||
|
||||
# License
|
||||
|
||||
AMAN is released under the [GNU General Public License v3](LICENSE)
|
||||
|
||||
110
aman/AMAN.py
Normal file
110
aman/AMAN.py
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
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
|
||||
from aman.com.Weather import Weather
|
||||
from aman.config.AircraftPerformance import AircraftPerformance
|
||||
from aman.config.Airport import Airport
|
||||
from aman.config.System import System
|
||||
from aman.sys.Worker import Worker
|
||||
|
||||
class AMAN:
|
||||
def findConfigPath():
|
||||
envvar = os.environ.get('AMAN_CONFIG_PATH')
|
||||
if None == envvar:
|
||||
print('No AMAN_CONFIG_PATH in environment variables found. Using execution directory.')
|
||||
path = os.getcwd()
|
||||
else:
|
||||
print('AMAN_CONFIG_PATH found.')
|
||||
path = envvar
|
||||
|
||||
print('Config-path: ' + path)
|
||||
return path
|
||||
|
||||
def __init__(self):
|
||||
# default initialization of members
|
||||
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'))
|
||||
print('Parsed System.ini')
|
||||
|
||||
# read the aircraft performance data
|
||||
self.AircraftPerformance = AircraftPerformance(os.path.join(configPath, 'PerformanceData.ini'))
|
||||
if None == self.AircraftPerformance:
|
||||
sys.stderr.write('No aircraft performance data found!')
|
||||
sys.exit(-1)
|
||||
else:
|
||||
print('Parsed PerformanceData.ini. Extracted ' + str(len(self.AircraftPerformance.Aircrafts)) + ' aircrafts')
|
||||
|
||||
# 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')
|
||||
for file in glob.glob(airportsPath):
|
||||
icao = os.path.splitext(os.path.basename(file))[0]
|
||||
|
||||
print('Parsing planner configuration for ' + icao)
|
||||
airportConfig = Airport(file, icao)
|
||||
|
||||
# initialize the worker thread
|
||||
worker = Worker(icao, airportConfig, self.Weather, self.AircraftPerformance, self.Receiver)
|
||||
self.Workers.append(worker)
|
||||
print('Started worker for ' + icao)
|
||||
|
||||
self.releaseLock()
|
||||
|
||||
# initialize the random number generator
|
||||
random.seed(time.time())
|
||||
|
||||
def acquireLock(self):
|
||||
if None != self.WorkersLock:
|
||||
self.WorkersLock.acquire()
|
||||
|
||||
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:
|
||||
worker.acquireLock()
|
||||
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
|
||||
0
aman/__init__.py
Normal file
0
aman/__init__.py
Normal file
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')
|
||||
162
aman/com/DwdCrawler.py
Normal file
162
aman/com/DwdCrawler.py
Normal file
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import datetime
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from datetime import datetime as dt
|
||||
|
||||
# @brief Checks the DWD pages for wind information
|
||||
# Format:
|
||||
# Provides next update tine (updateTime) of the DWD page in UTC
|
||||
# Provides a list of wind information (windData)
|
||||
# - organized as a list of tuples
|
||||
# - first element of tuple: GAFOR-IDs for the following wind information
|
||||
# - second element of tuple: list of tuples of wind data
|
||||
# - first element of wind data tuple: minimum altitude AMSL for this wind information
|
||||
# - second element of wind data tuple: wind direction
|
||||
# - third element of wind data tuple: wind speed (KT)
|
||||
class DwdCrawler():
|
||||
def __init__(self):
|
||||
self.UpdateTime = None
|
||||
self.WindData = None
|
||||
|
||||
def parseGaforAreas(areas : str):
|
||||
areas = areas.replace(':', '')
|
||||
areas = areas.split(' ')[1]
|
||||
areaIds = []
|
||||
|
||||
# some IDs are lists
|
||||
for segment in areas.split(','):
|
||||
# check if we have range definitions or single IDs
|
||||
borders = segment.split('-')
|
||||
if 2 == len(borders):
|
||||
areaIds.extend(range(int(borders[0]), int(borders[1]) + 1))
|
||||
else:
|
||||
areaIds.append(int(borders[0]))
|
||||
|
||||
return areaIds
|
||||
|
||||
def parseWindTableRow(row : str, table):
|
||||
# get the columns
|
||||
entries = row.split('|')
|
||||
|
||||
# check if the line is invalid or we have the header
|
||||
if 2 > len(entries) or 'AMSL' in entries[0]:
|
||||
return table
|
||||
|
||||
# parse the wind data
|
||||
windData = entries[1].strip().split(' ')[0].split('/')
|
||||
if 2 != len(windData):
|
||||
return table
|
||||
|
||||
# extend the table
|
||||
altitude = entries[0].strip()
|
||||
if 'FL' in altitude:
|
||||
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)
|
||||
|
||||
return table
|
||||
|
||||
def parseNextUpdateTime(line : str):
|
||||
entries = line.split(' ')
|
||||
if 4 <= len(entries):
|
||||
utcIndex = 2
|
||||
if 'UTC' in entries[len(entries) - 2]:
|
||||
utcIndex = len(entries) - 3
|
||||
elif 'UTC' in entries[len(entries) - 1]:
|
||||
utcIndex = len(entries - 2)
|
||||
|
||||
currentUtc = dt.utcfromtimestamp(int(time.time()))
|
||||
currentHour = int(currentUtc.strftime('%H'))
|
||||
|
||||
# check if we have a day overlap
|
||||
if currentHour > int(entries[utcIndex].split('.')[0]):
|
||||
nextDay = currentUtc + datetime.timedelta(days=1)
|
||||
date = nextDay.strftime('%Y-%m-%d')
|
||||
else:
|
||||
date = currentUtc.strftime('%Y-%m-%d')
|
||||
|
||||
# create the new UTC update time
|
||||
return dt.strptime(date + ' ' + entries[utcIndex] + '+0000', '%Y-%m-%d %H.%M%z')
|
||||
|
||||
def parseGaforPage(self, url : str):
|
||||
with urllib.request.urlopen(url) as site:
|
||||
data = site.read().decode('utf-8')
|
||||
site.close()
|
||||
|
||||
parsed = BeautifulSoup(data, features='lxml')
|
||||
|
||||
# search the info about the GAFOR areas
|
||||
content = None
|
||||
for element in parsed.body.find_all('pre'):
|
||||
content = element.text
|
||||
|
||||
# analyze the received data
|
||||
if None != content:
|
||||
windInformation = []
|
||||
nextUpdate = None
|
||||
windTable = []
|
||||
areaIds = None
|
||||
|
||||
# find all relevant information
|
||||
for line in content.splitlines():
|
||||
if '' == line:
|
||||
if 0 != len(windTable):
|
||||
for id in areaIds:
|
||||
windInformation.append([ id, windTable ])
|
||||
areaIds = None
|
||||
windTable = []
|
||||
elif line.startswith('GAFOR-Gebiete'):
|
||||
areaIds = DwdCrawler.parseGaforAreas(line)
|
||||
windTable = []
|
||||
elif None != areaIds:
|
||||
windTable = DwdCrawler.parseWindTableRow(line, windTable)
|
||||
elif 'Aktualisierung erfolgt um ' in line:
|
||||
nextUpdate = DwdCrawler.parseNextUpdateTime(line)
|
||||
|
||||
# return the collected information
|
||||
if 0 == len(windInformation) or None == nextUpdate:
|
||||
return None, None
|
||||
else:
|
||||
return nextUpdate, windInformation
|
||||
|
||||
def receiveWindData(self):
|
||||
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')
|
||||
site.close()
|
||||
|
||||
# find the pages of the GAFOR reports
|
||||
pages = []
|
||||
parsed = BeautifulSoup(data, features='lxml')
|
||||
for link in parsed.body.find_all('a', title=True):
|
||||
if 'node' in link['href'] and 'Flugwetterprognose' in link['title']:
|
||||
# remove the jsession from the link
|
||||
pages.append('https://www.dwd.de/' + link['href'].split(';')[0])
|
||||
|
||||
# receive the wind data
|
||||
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
|
||||
for gafor in wind:
|
||||
self.WindData[gafor[0]] = gafor[1]
|
||||
|
||||
# indicate that new wind data is available
|
||||
if None != self.UpdateTime:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
134
aman/com/Euroscope.py
Normal file
134
aman/com/Euroscope.py
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import zmq
|
||||
import zmq.auth
|
||||
|
||||
from aman.com import Communication_pb2
|
||||
from aman.config.Server import Server
|
||||
from threading import Thread
|
||||
|
||||
class ComThread(Thread):
|
||||
def __init__(self, com, aman):
|
||||
Thread.__init__(self)
|
||||
self.Com = com
|
||||
self.AMAN = aman
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
try:
|
||||
msg = self.Com.Socket.recv(zmq.NOBLOCK)
|
||||
|
||||
# parse the received message
|
||||
report = Communication_pb2.AircraftUpdate()
|
||||
report.ParseFromString(msg)
|
||||
|
||||
# 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.1)
|
||||
continue
|
||||
else:
|
||||
return
|
||||
|
||||
# @brief Receives and sends messages to EuroScope plugins
|
||||
class Euroscope:
|
||||
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')
|
||||
if False == os.path.isdir(serverKeyPath):
|
||||
sys.stderr.write('No directory for the server key found')
|
||||
sys.exit(-1)
|
||||
print('Path to the server key: ' + serverKeyPath)
|
||||
|
||||
clientKeyPath = os.path.join(os.path.join(configPath, 'keys'), 'clients')
|
||||
if False == os.path.isdir(clientKeyPath):
|
||||
sys.stderr.write('No directory for the client keys found')
|
||||
sys.exit(-1)
|
||||
print('Path to the client keys: ' + clientKeyPath)
|
||||
|
||||
# read the certificates
|
||||
keyPairPath = glob.glob(os.path.join(serverKeyPath, '*.key_secret'))
|
||||
if 1 != len(keyPairPath):
|
||||
sys.stderr.write('No public-private keypair found for the server certificate')
|
||||
sys.exit(-1)
|
||||
keyPair = zmq.auth.load_certificate(keyPairPath[0])
|
||||
|
||||
# initialize the receiver
|
||||
self.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))
|
||||
|
||||
def sendSequence(self, airport : str, inbounds, weather):
|
||||
if None == self.Socket:
|
||||
return
|
||||
|
||||
sequence = Communication_pb2.AircraftSequence()
|
||||
sequence.airport = airport
|
||||
|
||||
# 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])
|
||||
|
||||
# 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)
|
||||
47
aman/com/Weather.py
Normal file
47
aman/com/Weather.py
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import pytz
|
||||
import sys
|
||||
import time
|
||||
|
||||
from datetime import datetime as dt
|
||||
from threading import Thread
|
||||
|
||||
from aman.com.DwdCrawler import DwdCrawler
|
||||
import aman.config.Weather
|
||||
|
||||
class Weather(Thread):
|
||||
def __init__(self, config : aman.config.Weather.Weather):
|
||||
Thread.__init__(self)
|
||||
|
||||
self.NextUpdate = dt.utcfromtimestamp(int(time.time()))
|
||||
self.LastUpdateTried = None
|
||||
self.StopThread = False
|
||||
self.Provider = None
|
||||
|
||||
if 'DWD' == config.Provider.upper():
|
||||
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 currentClock():
|
||||
clock = dt.utcfromtimestamp(int(time.time())).replace(tzinfo = pytz.UTC)
|
||||
return clock
|
||||
|
||||
def run(self):
|
||||
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:
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
if None == self.LastUpdateTried or self.LastUpdateTried <= now:
|
||||
if True == self.Provider.receiveWindData():
|
||||
self.NextUpdate = self.Provider.UpdateTime
|
||||
print('Received new wind data')
|
||||
0
aman/com/__init__.py
Normal file
0
aman/com/__init__.py
Normal file
28
aman/config/AircraftPerformance.py
Normal file
28
aman/config/AircraftPerformance.py
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import configparser
|
||||
|
||||
from aman.types.PerformanceData import PerformanceData
|
||||
|
||||
class AircraftPerformance:
|
||||
def __init__(self, filepath : str):
|
||||
config = configparser.ConfigParser()
|
||||
config.read(filepath)
|
||||
self.Aircrafts = { }
|
||||
|
||||
# iterate over all entries
|
||||
for key in config:
|
||||
if 'DEFAULT' == key:
|
||||
continue
|
||||
|
||||
aircraft = PerformanceData(key)
|
||||
|
||||
aircraft.SpeedAboveFL240 = 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
|
||||
377
aman/config/Airport.py
Normal file
377
aman/config/Airport.py
Normal file
@@ -0,0 +1,377 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import configparser
|
||||
from datetime import timedelta
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
|
||||
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):
|
||||
if None == data.get('gngwildcard'):
|
||||
return None, None
|
||||
|
||||
# find the newest ESE file
|
||||
files = glob.glob(os.path.join(path, data['gngwildcard'] + '.ese'))
|
||||
latestEse = max(files, key=os.path.getctime)
|
||||
|
||||
# search for the corresponding SCT file
|
||||
latestSct = os.path.splitext(latestEse)[0] + '.sct'
|
||||
|
||||
# check if the files exist
|
||||
if False == os.path.isfile(latestEse) or False == os.path.isfile(latestSct):
|
||||
return None, None
|
||||
|
||||
return latestSct, latestEse
|
||||
|
||||
def parsePlanning(self, planning):
|
||||
if None == planning.get('routes'):
|
||||
return []
|
||||
return planning['routes'].split(':')
|
||||
|
||||
def 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:
|
||||
if 'DATA' == key:
|
||||
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))
|
||||
if None == sctFile or None == eseFile:
|
||||
sys.stderr.write('No GNG-files found')
|
||||
sys.exit(-1)
|
||||
|
||||
# parse the planning information
|
||||
if None == planningConfig or False == self.parsePlanning(planningConfig):
|
||||
sys.stderr.write('No planning configuration found')
|
||||
sys.exit(-1)
|
||||
requiredArrivalRoutes = self.parsePlanning(planningConfig)
|
||||
if 0 == len(requiredArrivalRoutes):
|
||||
sys.stderr.write('No valid planning configuration found')
|
||||
sys.exit(-1)
|
||||
|
||||
# parse the 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)
|
||||
|
||||
# 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 = {}
|
||||
32
aman/config/Server.py
Normal file
32
aman/config/Server.py
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import configparser;
|
||||
import sys
|
||||
|
||||
class Server():
|
||||
def __init__(self, config : configparser.ConfigParser):
|
||||
self.Address = None
|
||||
self.PortReceiver = None
|
||||
self.PortNotification = None
|
||||
self.WebUiUrl = None
|
||||
self.WebUiSequenceNotification = None
|
||||
self.WebUiConfigurationReceiver = None
|
||||
|
||||
# search the required sections
|
||||
for key in config:
|
||||
if 'address' == key:
|
||||
self.Address = config['address']
|
||||
elif 'portreceiver' == key:
|
||||
self.PortReceiver = int(config['portreceiver'])
|
||||
elif 'portnotification' == key:
|
||||
self.PortNotification = int(config['portnotification'])
|
||||
|
||||
if self.Address is None:
|
||||
sys.stderr.write('No server-address configuration found!')
|
||||
sys.exit(-1)
|
||||
if self.PortReceiver is None:
|
||||
sys.stderr.write('No server-port-receiver configuration found!')
|
||||
sys.exit(-1)
|
||||
if self.PortNotification is None:
|
||||
sys.stderr.write('No server-port-notification configuration found!')
|
||||
sys.exit(-1)
|
||||
30
aman/config/System.py
Normal file
30
aman/config/System.py
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import configparser
|
||||
import sys
|
||||
|
||||
from aman.config.Server import Server
|
||||
from aman.config.Weather import Weather
|
||||
|
||||
class System:
|
||||
def __init__(self, filepath : str):
|
||||
config = configparser.ConfigParser()
|
||||
config.read(filepath)
|
||||
|
||||
# search the required sections
|
||||
serverSectionAvailable = False
|
||||
for key in config:
|
||||
if 'SERVER' == key:
|
||||
serverSectionAvailable = True
|
||||
elif 'WEATHER' == key:
|
||||
weatherSectionAvailable = True
|
||||
|
||||
if not serverSectionAvailable:
|
||||
sys.stderr.write('No server-configuration section found!')
|
||||
sys.exit(-1)
|
||||
if not weatherSectionAvailable:
|
||||
sys.stderr.write('No weather-configuration section found!')
|
||||
sys.exit(1)
|
||||
|
||||
self.Server = Server(config['SERVER'])
|
||||
self.Weather = Weather(config['WEATHER'])
|
||||
17
aman/config/Weather.py
Normal file
17
aman/config/Weather.py
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import configparser;
|
||||
import sys
|
||||
|
||||
class Weather():
|
||||
def __init__(self, config : configparser.ConfigParser):
|
||||
self.Provider = None
|
||||
|
||||
# search the required sections
|
||||
for key in config:
|
||||
if 'provider' == key:
|
||||
self.Provider = config['provider']
|
||||
|
||||
if self.Provider is None:
|
||||
sys.stderr.write('No weather-provider configuration found!')
|
||||
sys.exit(-1)
|
||||
0
aman/config/__init__.py
Normal file
0
aman/config/__init__.py
Normal file
180
aman/formats/SctEseFormat.py
Normal file
180
aman/formats/SctEseFormat.py
Normal file
@@ -0,0 +1,180 @@
|
||||
#!/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:
|
||||
def readFile(filename : str):
|
||||
fileBlocks = {}
|
||||
block = None
|
||||
|
||||
# read the file line by line and create segments based on []-entries
|
||||
with open(filename) as file:
|
||||
for line in file:
|
||||
line = line.strip()
|
||||
|
||||
# found a new segment
|
||||
if line.startswith('['):
|
||||
block = line[1:-1]
|
||||
fileBlocks.setdefault(block, [])
|
||||
# append the last backend
|
||||
elif None != block and 0 != len(line):
|
||||
fileBlocks[block].append(line)
|
||||
|
||||
return fileBlocks
|
||||
|
||||
def parseWaypoint(waypoint : str, nameIdx : int, latitudeIdx : int, longitudeIdx : int):
|
||||
split = list(filter(None, waypoint.split(' ')))
|
||||
if len(split) <= longitudeIdx:
|
||||
sys.stderr.write('Invalid waypoint format: ' + waypoint)
|
||||
sys.exit(-1)
|
||||
return Waypoint(name = split[nameIdx], latitude = split[latitudeIdx], longitude = split[longitudeIdx])
|
||||
|
||||
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
|
||||
|
||||
for key in config:
|
||||
if 'VOR' == key:
|
||||
foundVOR = True
|
||||
elif 'NDB' == key:
|
||||
foundNDB = True
|
||||
elif 'FIXES' == key:
|
||||
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')
|
||||
sys.exit(-1)
|
||||
if False == foundNDB:
|
||||
sys.stderr.write('Unable to find NDB-entries in the sector file')
|
||||
sys.exit(-1)
|
||||
if False == foundFix:
|
||||
sys.stderr.write('Unable to find FIX-entries in the sector file')
|
||||
sys.exit(-1)
|
||||
if False == foundAirports:
|
||||
sys.stderr.write('Unable to find AIRPORT-entries in the sector file')
|
||||
sys.exit(-1)
|
||||
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)
|
||||
for waypoint in config['NDB']:
|
||||
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 1, 2)
|
||||
self.Waypoints.setdefault(waypoint.Name, []).append(waypoint)
|
||||
for waypoint in config['FIXES']:
|
||||
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 1, 2)
|
||||
self.Waypoints.setdefault(waypoint.Name, []).append(waypoint)
|
||||
|
||||
# extract the airports
|
||||
for airport in config['AIRPORT']:
|
||||
airport = SctEseFormat.parseWaypoint(airport, 0, 2, 3)
|
||||
self.Airports.setdefault(airport.Name, []).append(airport)
|
||||
|
||||
# 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:
|
||||
return None
|
||||
|
||||
# find all waypoints
|
||||
waypoints = []
|
||||
route = list(filter(None, split[4].split(' ')))
|
||||
for waypoint in route:
|
||||
# find the waypoint in the route
|
||||
coordinates = self.Waypoints[waypoint]
|
||||
# no waypoint with this name defined
|
||||
if None == coordinates:
|
||||
sys.stderr.write('Unable to find waypoint ' + waypoint)
|
||||
sys.exit(-1)
|
||||
# multiple waypoints, but use Haversine distance to distinct between candidates
|
||||
elif 1 != len(coordinates):
|
||||
minDistance = sys.float_info.max
|
||||
nearest = None
|
||||
|
||||
# we assume that waypoints with the same name are not close each other
|
||||
for coordinate in coordinates:
|
||||
distance = coordinate.haversine(airport)
|
||||
# found a closer waypoint
|
||||
if minDistance > distance:
|
||||
minDistance = distance
|
||||
nearest = coordinate
|
||||
|
||||
if None == nearest:
|
||||
sys.stderr.write('Unable to find a close waypoint for ' + waypoint)
|
||||
sys.exit(-1)
|
||||
|
||||
waypoints.append(copy.deepcopy(nearest))
|
||||
# extend the list of waypoints
|
||||
else:
|
||||
waypoints.append(copy.deepcopy(coordinates[0]))
|
||||
|
||||
# create the arrival route
|
||||
return ArrivalRoute(split[3], split[2], waypoints)
|
||||
|
||||
def extractArrivalRoutes(self, eseFilepath : str, airport : str, allowedRoutes : list):
|
||||
config = SctEseFormat.readFile(eseFilepath)
|
||||
foundSidsStars = False
|
||||
|
||||
# search the airport in the extracted list
|
||||
if not airport in self.Airports:
|
||||
sys.stderr.write('Unable to find the requested airport')
|
||||
sys.exit(-1)
|
||||
airport = self.Airports[airport][0]
|
||||
|
||||
for key in config:
|
||||
if 'SIDSSTARS' == key:
|
||||
foundSidsStars = True
|
||||
|
||||
if False == foundSidsStars:
|
||||
sys.stderr.write('Unable to find SIDSSTARS-entries in the sector file')
|
||||
sys.exit(-1)
|
||||
|
||||
# parse all arrival routes
|
||||
for line in config['SIDSSTARS']:
|
||||
route = self.parseArrivalRoute(line, airport)
|
||||
if None != route and route.Name in allowedRoutes:
|
||||
self.ArrivalRoutes.setdefault(route.Runway, []).append(route)
|
||||
|
||||
def __init__(self, sctFilepath : str, eseFilepath : str, airport : str, allowedRoutes : list):
|
||||
self.ArrivalRoutes = {}
|
||||
self.Waypoints = {}
|
||||
self.Airports = {}
|
||||
self.Runways = {}
|
||||
|
||||
self.extractSctInformation(sctFilepath)
|
||||
self.extractArrivalRoutes(eseFilepath, airport, allowedRoutes)
|
||||
0
aman/formats/__init__.py
Normal file
0
aman/formats/__init__.py
Normal file
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
|
||||
184
aman/sys/WeatherModel.py
Normal file
184
aman/sys/WeatherModel.py
Normal file
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from aman.com.Weather import Weather
|
||||
|
||||
import math
|
||||
import scipy.interpolate
|
||||
|
||||
class WeatherModel:
|
||||
def __init__(self, gaforId, weather : Weather):
|
||||
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/
|
||||
altitudes = [
|
||||
50000,
|
||||
45000,
|
||||
40000,
|
||||
38000,
|
||||
36000,
|
||||
34000,
|
||||
32000,
|
||||
30000,
|
||||
28000,
|
||||
26000,
|
||||
24000,
|
||||
22000,
|
||||
20000,
|
||||
18000,
|
||||
16000,
|
||||
15000,
|
||||
14000,
|
||||
13000,
|
||||
12000,
|
||||
11000,
|
||||
10000,
|
||||
9000,
|
||||
8000,
|
||||
7000,
|
||||
6000,
|
||||
5000,
|
||||
4000,
|
||||
3000,
|
||||
2000,
|
||||
1000,
|
||||
0
|
||||
]
|
||||
densities = [
|
||||
0.18648,
|
||||
0.23714,
|
||||
0.24617,
|
||||
0.33199,
|
||||
0.36518,
|
||||
0.39444,
|
||||
0.42546,
|
||||
0.45831,
|
||||
0.402506,
|
||||
0.432497,
|
||||
0.464169,
|
||||
0.60954,
|
||||
0.65269,
|
||||
0.69815,
|
||||
0.74598,
|
||||
0.77082,
|
||||
0.79628,
|
||||
0.82238,
|
||||
0.84914,
|
||||
0.87655,
|
||||
0.90464,
|
||||
0.93341,
|
||||
0.96287,
|
||||
0.99304,
|
||||
1.02393,
|
||||
1.05555,
|
||||
1.08791,
|
||||
1.12102,
|
||||
1.1549,
|
||||
1.18955,
|
||||
1.225
|
||||
]
|
||||
self.densityModel = scipy.interpolate.interp1d(altitudes, densities)
|
||||
|
||||
def calculateTAS(self, altitude : int, ias : int):
|
||||
if altitude >= 50000:
|
||||
altitude = 49999
|
||||
if altitude <= 0:
|
||||
altitude = 1
|
||||
|
||||
# calculation based on https://aerotoolbox.com/airspeed-conversions/
|
||||
return ias * math.sqrt(1.225 / self.densityModel(altitude).item())
|
||||
|
||||
def updateWindModel(self):
|
||||
if None == self.Weather or None == self.Weather.Provider:
|
||||
return
|
||||
|
||||
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:
|
||||
self.Altitudes = []
|
||||
self.Directions = []
|
||||
self.Windspeeds = []
|
||||
|
||||
# collect the data for the wind model
|
||||
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]
|
||||
|
||||
# calculate the models
|
||||
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 interpolateWindData(self, altitude : int):
|
||||
self.updateWindModel()
|
||||
|
||||
# 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()
|
||||
else:
|
||||
speed = 0
|
||||
direction = 0
|
||||
|
||||
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
|
||||
140
aman/sys/Worker.py
Normal file
140
aman/sys/Worker.py
Normal file
@@ -0,0 +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, icao : str, configuration : Airport, weather : Weather,
|
||||
performance : PerformanceData, euroscope : Euroscope):
|
||||
Thread.__init__(self)
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
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 releaseLock(self):
|
||||
if None != self.UpdateLock:
|
||||
self.UpdateLock.release()
|
||||
|
||||
def run(self):
|
||||
counter = 0
|
||||
|
||||
while None == self.StopThread:
|
||||
time.sleep(1)
|
||||
counter += 1
|
||||
if 0 != (counter % 60):
|
||||
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()
|
||||
0
aman/sys/__init__.py
Normal file
0
aman/sys/__init__.py
Normal file
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
99
aman/tools/KeyPairCreator.py
Normal file
99
aman/tools/KeyPairCreator.py
Normal file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
import os
|
||||
import sys
|
||||
from typing import Tuple
|
||||
import zmq.auth
|
||||
|
||||
# @brief Creates a new keypair for ZMQ encryption
|
||||
# @param[in] directory The location where to store the keys
|
||||
# @return The public and private key tuple
|
||||
def KeyPairCreator(directory: str, server: bool) -> Tuple[str, str]:
|
||||
if not server:
|
||||
target = 'client'
|
||||
else:
|
||||
target = 'server'
|
||||
|
||||
public, private = zmq.auth.create_certificates(directory, target)
|
||||
return public, private
|
||||
|
||||
def str2bool(value):
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
elif value.lower() in ('yes', 'true', 't', 'y', '1'):
|
||||
return True
|
||||
elif value.lower() in ('no', 'false', 'f', 'n', '0'):
|
||||
return False
|
||||
else:
|
||||
raise argparse.ArgumentTypeError('Boolean value expected')
|
||||
|
||||
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', 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
|
||||
_, 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)
|
||||
147
aman/tools/SkybraryAircraftCrawler.py
Normal file
147
aman/tools/SkybraryAircraftCrawler.py
Normal file
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
import configparser
|
||||
import os
|
||||
import urllib.request
|
||||
from bs4 import BeautifulSoup
|
||||
from aman.types.PerformanceData import PerformanceData
|
||||
|
||||
def findAircraftPages(rooturl : str, suburl : str):
|
||||
aircrafts = []
|
||||
|
||||
with urllib.request.urlopen(rooturl + suburl) as site:
|
||||
data = site.read().decode('utf-8')
|
||||
site.close()
|
||||
|
||||
parsed = BeautifulSoup(data, features='lxml')
|
||||
|
||||
for link in parsed.body.find_all('a', title=True):
|
||||
split = link['href'].split('/')
|
||||
if 3 == len(split) and split[2] == link['title'] and 'Category' not in link['title'] and 'Special' not in link['href']:
|
||||
aircrafts.append(rooturl + link['href'])
|
||||
|
||||
for link in parsed.body.find_all('a', attrs={ 'title': 'Category:Aircraft' }):
|
||||
if 'previous' not in link.text:
|
||||
aircrafts.extend(findAircraftPages(rooturl, link['href']))
|
||||
|
||||
return aircrafts
|
||||
|
||||
def findAndParseEntry(tableRow, startIdx, substring, default):
|
||||
while 0 < startIdx:
|
||||
if substring in tableRow[startIdx].text:
|
||||
split = tableRow[startIdx].text.split(' ')
|
||||
if 1 >= len(split):
|
||||
return default, startIdx - 2
|
||||
else:
|
||||
return int(split[0]), startIdx - 2
|
||||
else:
|
||||
startIdx -= 1
|
||||
|
||||
return 0, -1
|
||||
|
||||
def findAndParseSpeedEntry(tableRow, startIdx, default):
|
||||
return findAndParseEntry(tableRow, startIdx, 'kts', default)
|
||||
|
||||
def findAndParseRodEntry(tableRow, startIdx, default):
|
||||
return findAndParseEntry(tableRow, startIdx, 'ft/min', default)
|
||||
|
||||
def parsePerformanceEntries(tableRowSpeeds, tableRowRODs):
|
||||
speeds = []
|
||||
rods = []
|
||||
|
||||
# parse the speed data
|
||||
idx = len(tableRowSpeeds) - 1
|
||||
while 0 < idx:
|
||||
parsed = findAndParseSpeedEntry(tableRowSpeeds, idx, 140 if 0 == len(speeds) else 250)
|
||||
if 0 < idx:
|
||||
speeds.append(parsed[0])
|
||||
idx = parsed[1]
|
||||
|
||||
# parse the ROD data
|
||||
idx = len(tableRowRODs) - 1
|
||||
while 0 < idx:
|
||||
parsed = findAndParseRodEntry(tableRowRODs, idx, 2000)
|
||||
if 0 < idx:
|
||||
rods.append(parsed[0])
|
||||
idx = parsed[1]
|
||||
|
||||
return speeds, rods
|
||||
|
||||
def parsePerformanceData(url : str):
|
||||
with urllib.request.urlopen(url) as site:
|
||||
data = site.read().decode('utf-8')
|
||||
site.close()
|
||||
|
||||
# check if we find the ICAO code
|
||||
parsed = BeautifulSoup(data, features='lxml')
|
||||
icao = parsed.body.find('h5', attrs={ 'id' : 'siteSub', 'class' : 'subtitle'})
|
||||
if None == icao or '' == icao.text:
|
||||
return False, None
|
||||
|
||||
aircraft = PerformanceData(icao.text)
|
||||
performanceTable = parsed.body.find('table', attrs={ 'class' : 'wikitable', 'style' : 'font-size: 90%;' })
|
||||
if None == performanceTable or None == performanceTable.find_all('tr')[1] or None == performanceTable.find_all('tr')[2]:
|
||||
return False, None
|
||||
|
||||
speeds, rods = parsePerformanceEntries(performanceTable.find_all('tr')[1].find_all('td'),
|
||||
performanceTable.find_all('tr')[2].find_all('td'))
|
||||
if 10 > len(speeds):
|
||||
speeds.insert(1, speeds[1])
|
||||
|
||||
# create the speed data
|
||||
if len(speeds) >= 4:
|
||||
aircraft.speedApproach = speeds[0]
|
||||
aircraft.speedBelowFL100 = speeds[1]
|
||||
aircraft.speedAboveFL100 = speeds[2]
|
||||
aircraft.speedAboveFL240 = speeds[3]
|
||||
# create the ROD data
|
||||
if len(rods) >= 3:
|
||||
aircraft.rodBelowFL100 = rods[0]
|
||||
aircraft.rodAboveFL100 = rods[1]
|
||||
aircraft.rodAboveFL240 = rods[2]
|
||||
|
||||
return len(speeds) >= 4 and len(rods) >= 3, aircraft
|
||||
|
||||
if __name__ == '__main__':
|
||||
# create the commandline parser
|
||||
parser = argparse.ArgumentParser(description='Extract the aircraft performace data')
|
||||
parser.add_argument('directory', help='Directory where to store the performance data configuration')
|
||||
args = parser.parse_args()
|
||||
|
||||
# create the directory if it does not exist
|
||||
if not os.path.exists(args.directory):
|
||||
os.makedirs(args.directory)
|
||||
|
||||
# parse the aircrafts
|
||||
links = findAircraftPages('https://www.skybrary.aero', '/index.php?title=Category:Aircraft')
|
||||
print('Found ' + str(len(links)) + ' aircrafts')
|
||||
|
||||
aircrafts = []
|
||||
for link in links:
|
||||
valid, aircraft = parsePerformanceData(link)
|
||||
|
||||
if False == valid:
|
||||
print('Unable to find performance data for ' + link)
|
||||
continue
|
||||
|
||||
aircrafts.append(aircraft)
|
||||
|
||||
print('Successfully parsed ' + str(len(aircrafts)) + ' of ' + str(len(links)) + ' aircrafts')
|
||||
|
||||
# create the configuration file
|
||||
config = configparser.ConfigParser()
|
||||
for aircraft in aircrafts:
|
||||
config[aircraft.icao] = {
|
||||
'speedAboveFL240' : aircraft.speedAboveFL240,
|
||||
'rodAboveFL240' : aircraft.rodAboveFL240,
|
||||
'speedAboveFL100' : aircraft.speedAboveFL100,
|
||||
'rodAboveFL100' : aircraft.rodAboveFL100,
|
||||
'speedBelowFL100' : aircraft.speedBelowFL100,
|
||||
'rodBelowFL100' : aircraft.rodBelowFL100,
|
||||
'speedApproach' : aircraft.speedApproach
|
||||
}
|
||||
|
||||
# write the configuration data
|
||||
with open(args.directory + '/PerformanceData.ini', 'w') as file:
|
||||
config.write(file)
|
||||
0
aman/tools/__init__.py
Normal file
0
aman/tools/__init__.py
Normal file
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)
|
||||
11
aman/types/ArrivalRoute.py
Normal file
11
aman/types/ArrivalRoute.py
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
class ArrivalRoute:
|
||||
def __init__(self, name : str, runway : str, waypoints : list):
|
||||
self.Name = name
|
||||
self.Runway = runway
|
||||
self.Iaf = waypoints[0]
|
||||
self.Route = waypoints
|
||||
|
||||
def __str__(self):
|
||||
return 'Name: ' + self.Name + ', IAF: ' + self.Iaf.Name + ', RWY: ' + self.Runway
|
||||
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)
|
||||
|
||||
|
||||
45
aman/types/Inbound.py
Normal file
45
aman/types/Inbound.py
Normal file
@@ -0,0 +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, 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']
|
||||
30
aman/types/PerformanceData.py
Normal file
30
aman/types/PerformanceData.py
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
class PerformanceData:
|
||||
def __init__(self, icao : str):
|
||||
self.Icao = icao
|
||||
self.SpeedAboveFL240 = 0.0
|
||||
self.SpeedAboveFL100 = 0.0
|
||||
self.SpeedBelowFL100 = 0.0
|
||||
self.SpeedApproach = 0.0
|
||||
self.RodAboveFL240 = 0.0
|
||||
self.RodAboveFL100 = 0.0
|
||||
self.RodBelowFL100 = 2000.0
|
||||
|
||||
def 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)
|
||||
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)
|
||||
78
aman/types/Waypoint.py
Normal file
78
aman/types/Waypoint.py
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import numpy as np
|
||||
import pyproj
|
||||
|
||||
class Waypoint:
|
||||
def dms2dd(coordinate : str):
|
||||
split = coordinate.split('.')
|
||||
if 4 != len(split):
|
||||
return 0.0
|
||||
|
||||
direction = split[0][1]
|
||||
degrees = float(split[0][1:])
|
||||
minutes = float(split[1])
|
||||
seconds = float(split[2]) * (float(split[3]) / 1000.0)
|
||||
|
||||
dd = degrees + minutes / 60.0 + seconds / (60 * 60)
|
||||
if 'E' == direction or 'S' == direction:
|
||||
dd *= -1.0
|
||||
|
||||
return dd
|
||||
|
||||
def 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])
|
||||
|
||||
def haversine(self, other):
|
||||
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 ])
|
||||
0
aman/types/__init__.py
Normal file
0
aman/types/__init__.py
Normal file
BIN
external/bin/protoc.exe
vendored
Normal file
BIN
external/bin/protoc.exe
vendored
Normal file
Binary file not shown.
32
external/licenses/ProtoBuf-3.18.1
vendored
Normal file
32
external/licenses/ProtoBuf-3.18.1
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
Copyright 2008 Google Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Code generated by the Protocol Buffer compiler is owned by the owner
|
||||
of the input file used when generating it. This code is not
|
||||
standalone and requires a support library to be linked with it. This
|
||||
support library is itself covered by the above license.
|
||||
104
setup.py
Normal file
104
setup.py
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from setuptools import setup
|
||||
from distutils.command.clean import clean as _clean
|
||||
from distutils.command.build_py import build_py_2to3 as _build_py
|
||||
|
||||
# @brief Creates Protobuf python files to encode and decode messages
|
||||
# @param[in] source The protobuf source file
|
||||
def generateProtobuf(source):
|
||||
output = source.replace('.proto', '_pb2.py').replace('src/protobuf/', 'aman/com/')
|
||||
|
||||
if (not os.path.exists(output) or (os.path.exists(source) and os.path.getmtime(source) > os.path.getmtime(output))):
|
||||
print('Generating %s...' % output)
|
||||
|
||||
if not os.path.exists(source):
|
||||
sys.stderr.write('Cannot find %s' % source)
|
||||
sys.exit(-1)
|
||||
|
||||
if not os.path.exists('external/bin/protoc.exe'):
|
||||
sys.stderr.write('Cannot find proto-compiler')
|
||||
sys.exit(-1)
|
||||
|
||||
command = [ 'external/bin/protoc.exe', '-Isrc/protobuf/', '-I.', '--python_out=aman/com/', source]
|
||||
if 0 != subprocess.call(command):
|
||||
sys.exit(-1)
|
||||
|
||||
# check if we need to replace some import commands
|
||||
replaced = False
|
||||
content = open(output, 'r').read()
|
||||
for entry in re.findall('import.[A-Z].*.as.*', content):
|
||||
content = content.replace(entry, 'from . ' + entry)
|
||||
replaced = True
|
||||
|
||||
# update the content
|
||||
if replaced:
|
||||
with open(output, 'w') as file:
|
||||
file.write(content)
|
||||
|
||||
# @brief Cleans up all auto-generated files and folders
|
||||
# @param[in] _clean Instance of setuptools to clean up the system
|
||||
class clean(_clean):
|
||||
def run(self):
|
||||
for (dirpath, dirnames, filenames) in os.walk('.'):
|
||||
for filename in filenames:
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
if filepath.endswith('_pb2.py') or filepath.endswith('.pyc'):
|
||||
os.remove(filepath)
|
||||
for dirname in dirnames:
|
||||
if 'Arrival_MANager.egg-info' == dirname or 'build' == dirname or 'dist' == dirname or '__pycache__' == dirname:
|
||||
shutil.rmtree(os.path.join(dirpath, dirname))
|
||||
_clean.run(self)
|
||||
|
||||
# @brief Generates the python files and folders to set up the development/runtime environment
|
||||
# @param[in] _build_py Instance of setuptools to build the system
|
||||
class build_py(_build_py):
|
||||
def run(self):
|
||||
generateProtobuf('src/protobuf/Aircraft.proto')
|
||||
generateProtobuf('src/protobuf/AircraftReport.proto')
|
||||
generateProtobuf('src/protobuf/AircraftSchedule.proto')
|
||||
generateProtobuf('src/protobuf/BaseTypes.proto')
|
||||
generateProtobuf('src/protobuf/Communication.proto')
|
||||
_build_py.run(self)
|
||||
|
||||
with open('README.md', 'r') as f:
|
||||
longDescription = f.read()
|
||||
|
||||
setup(
|
||||
name = 'Arrival MANager',
|
||||
version = '0.1.0',
|
||||
packages = [
|
||||
'aman',
|
||||
'aman.com',
|
||||
'aman.config',
|
||||
'aman.formats',
|
||||
'aman.sys',
|
||||
'aman.sys.aco',
|
||||
'aman.tools',
|
||||
'aman.types'
|
||||
],
|
||||
namespace_packages = [ 'aman' ],
|
||||
description = 'AMAN optimization backend',
|
||||
long_description = longDescription,
|
||||
author = 'Sven Czarnian, Pascal Seeler',
|
||||
author_email = 'devel@svcz.de',
|
||||
license = 'GPLv3',
|
||||
cmdclass = { 'clean': clean, 'build_py': build_py },
|
||||
install_requires=[
|
||||
'argparse',
|
||||
'bs4',
|
||||
'configparser',
|
||||
'numpy',
|
||||
'protobuf',
|
||||
'pyzmq',
|
||||
'scipy',
|
||||
'setuptools',
|
||||
'flask',
|
||||
'flask-cors'
|
||||
]
|
||||
)
|
||||
1
src/protobuf
Submodule
1
src/protobuf
Submodule
Submodule src/protobuf added at 893e012b3f
Reference in New Issue
Block a user