301 Commits

Author SHA1 Message Date
3237f20994 submodule 2023-07-14 21:48:55 +02:00
Sven Czarnian
6426aa2bd4 send the IAFs as well 2021-12-26 10:41:55 +01:00
Sven Czarnian
716c1090f3 add a comment 2021-12-26 10:41:37 +01:00
Sven Czarnian
fdf38b46dd fix a renaming issue 2021-12-26 10:41:06 +01:00
Sven Czarnian
c9937d39c2 fix sporadic system crashes 2021-12-25 22:37:20 +01:00
Sven Czarnian
357f6e7b11 do not minimize the TTG 2021-12-25 09:00:13 +01:00
Sven Czarnian
2fdb73e32e do not ignore inbounds that respawned inside the freezed time 2021-12-25 08:59:59 +01:00
Sven Czarnian
bf6417c66c add a flag that inidicates if the flight has all relevant information 2021-12-25 08:58:12 +01:00
Sven Czarnian
5ef3d4e40a fix a race condition crash 2021-12-23 17:49:10 +01:00
Sven Czarnian
8ac4977c08 initialize all required information to avoid race conditions between in plugins and the web interface 2021-12-23 16:29:25 +01:00
Sven Czarnian
64fcb8ed01 use deep copies for the optimization to avoid the visualization of intermediate sequences 2021-12-23 16:28:31 +01:00
Sven Czarnian
57867a2e21 fix a crash 2021-12-22 15:47:11 +01:00
Sven Czarnian
f6736133a7 add shall-should-may information to the sequence 2021-12-22 15:13:35 +01:00
Sven Czarnian
6dc4f6de72 provide shall-should-may information 2021-12-22 15:13:18 +01:00
Sven Czarnian
50b60915f2 extend the configuration to provide the colors 2021-12-22 14:31:11 +01:00
Sven Czarnian
857f278afe addional planning information 2021-12-22 14:30:56 +01:00
Sven Czarnian
8efed19e34 parse the WebUI configuration 2021-12-22 14:30:22 +01:00
Sven Czarnian
b78e1952ee use the new member structure to provide the data 2021-12-22 13:43:52 +01:00
Sven Czarnian
8d196129f0 fix a system crash 2021-12-22 13:43:29 +01:00
Sven Czarnian
97c7173313 add the IAF spacing constraint to the arrival estimation 2021-12-22 13:29:36 +01:00
Sven Czarnian
b441b945b9 code formatting 2021-12-22 13:29:21 +01:00
Sven Czarnian
d9bc25a507 increase the overall performance 2021-12-22 13:29:08 +01:00
Sven Czarnian
f83febab3a add a function to register a new inbound 2021-12-22 13:28:56 +01:00
Sven Czarnian
4f69df6cc7 fix issues with the fixed inbounds 2021-12-22 13:27:58 +01:00
Sven Czarnian
1b53084e8c add better differences to check if arrival candidates exist 2021-12-22 13:27:35 +01:00
Sven Czarnian
4439f91633 add a missing member variable 2021-12-22 13:26:40 +01:00
Sven Czarnian
13837fdb62 use a new function to create the fixed preceedings and use the information in the runway manager 2021-12-21 10:45:32 +01:00
Sven Czarnian
a2ea26ab5d introduce the configuration for the IAF spacing 2021-12-21 10:42:16 +01:00
Sven Czarnian
83acbdc3b5 remove unused variables and rename a parameter 2021-12-20 14:24:27 +01:00
Sven Czarnian
f4fa9eeb18 fix the issues with the time delta estimation 2021-12-19 13:26:49 +01:00
Sven Czarnian
fc84aab0f2 robust error handling to avoid later crashes 2021-12-19 13:06:03 +01:00
Sven Czarnian
91683ec899 code formatting 2021-12-19 12:27:44 +01:00
Sven Czarnian
8301fba2d2 fix a setup.py build error 2021-12-19 12:24:41 +01:00
Sven Czarnian
e265629439 handle the requested runways in the optimization 2021-12-19 12:24:26 +01:00
Sven Czarnian
8ebeef6938 update the interface description 2021-12-19 12:23:54 +01:00
Sven Czarnian
e1eb50f6d3 avoid crashes due to invalid WTC entries 2021-12-17 13:32:07 +01:00
Sven Czarnian
7006e8b835 fix some table issues 2021-12-17 13:31:54 +01:00
Sven Czarnian
dda7a90ef7 do not ignore flights that passed the IAF 2021-12-17 13:05:53 +01:00
Sven Czarnian
d1e90cfd22 remove useless print message 2021-12-16 16:08:11 +01:00
Sven Czarnian
78d61eb6cc fix rare crashes 2021-12-16 15:59:26 +01:00
Sven Czarnian
1acec75d08 fix a wrong function call 2021-12-16 15:59:13 +01:00
Sven Czarnian
ae96e5be6b abort if we found the optimal solution 2021-12-16 10:24:10 +01:00
Sven Czarnian
bb6cacd898 fix an issue in the global update 2021-12-16 10:23:40 +01:00
Sven Czarnian
cd5c21d099 use the shortcut-path as the ITA for the optimization, but use the full path as the ITA itself 2021-12-16 10:22:43 +01:00
Sven Czarnian
6fd4f4da1b fix a referencing bug 2021-12-16 08:37:11 +01:00
Sven Czarnian
1bf7d850eb fix different interpretations of the freezed index 2021-12-15 13:18:16 +01:00
Sven Czarnian
1e031dcb7c fix a wrong comment 2021-12-15 13:17:40 +01:00
Sven Czarnian
cbf033f4b6 remove the debug message 2021-12-15 11:21:02 +01:00
Sven Czarnian
760145731d fix a bug in the ETA estimation 2021-12-15 09:29:12 +01:00
Sven Czarnian
5242fe0d87 use the optimization thresholds to calculate the TTG 2021-12-15 09:28:59 +01:00
Sven Czarnian
72959a8e26 add optimization thresholds 2021-12-15 09:27:53 +01:00
Sven Czarnian
5c2765d67a remove unused code 2021-12-15 09:27:41 +01:00
Sven Czarnian
bb2aaeba7a use the best initial ITA 2021-12-14 11:26:57 +01:00
Sven Czarnian
4f21f8968b fix the TTG calculation 2021-12-14 11:26:25 +01:00
Sven Czarnian
ad129dc2b6 fix the prediction calculation 2021-12-14 11:25:51 +01:00
Sven Czarnian
01b12b4398 do not overwrite the last waypoint if it is already set up 2021-12-14 11:25:27 +01:00
Sven Czarnian
ba34ff97a9 add the flight time until the IAF 2021-12-14 11:25:02 +01:00
Sven Czarnian
a85bcac1e8 remove useless TODO 2021-12-14 11:24:02 +01:00
Sven Czarnian
4fbe9d1060 refactor the ETA estimation 2021-12-14 11:23:18 +01:00
Sven Czarnian
74b8ec33d5 update the report time to allow predictions 2021-12-14 11:23:03 +01:00
Sven Czarnian
52aefd0966 minimize the TTL to provide realistic TTLs 2021-12-09 12:30:07 +01:00
Sven Czarnian
4797cef3f7 decrease the sleep time to react in time 2021-12-09 12:29:51 +01:00
Sven Czarnian
92b7e9e429 use the new protocol 2021-12-09 11:34:19 +01:00
Sven Czarnian
70be822e2d fix a system crash 2021-12-09 11:34:06 +01:00
Sven Czarnian
a5cb8914b5 update the interface 2021-12-09 11:33:56 +01:00
Sven Czarnian
0a891b99a5 add the ITA for visualization purposes 2021-12-09 09:27:19 +01:00
Sven Czarnian
fb40b0aad9 handle error codes and receive all outputs 2021-12-09 09:19:10 +01:00
Sven Czarnian
9627ae34b7 extend the interface to provide all available runways 2021-12-09 09:18:20 +01:00
Sven Czarnian
efae307e84 add cross origin to fix issues in browsers 2021-12-09 09:17:58 +01:00
Sven Czarnian
7257ab2956 split up the initializations 2021-12-03 11:51:27 +01:00
Sven Czarnian
9b9746e9bb add documentation 2021-12-03 11:51:04 +01:00
Sven Czarnian
848d89a918 define the route to create a new key and send information 2021-12-02 23:06:51 +01:00
Sven Czarnian
6aa2009cce update the key create to call it via the web interface to create new keys 2021-12-02 23:06:32 +01:00
Sven Czarnian
22216b6627 add a warning message to show if the execution exceeded the maximum time 2021-11-29 19:25:19 +01:00
Sven Czarnian
6d5e635d42 remove debug messages 2021-11-29 17:58:11 +01:00
Sven Czarnian
50f3e28baf remove a debug message 2021-11-29 17:58:02 +01:00
Sven Czarnian
f14c6735cc allow only delays to avoid arms races between controller and optimizer by implicite ITA-improvements on center 2021-11-29 17:57:20 +01:00
Sven Czarnian
2b4200ba58 remove a debug message 2021-11-29 17:56:28 +01:00
Sven Czarnian
7636c549db fix a potential crash 2021-11-29 17:56:08 +01:00
Sven Czarnian
836ff0a8b2 use as much aircrafts as possible 2021-11-29 17:13:28 +01:00
Sven Czarnian
8277721edb use only 5% speed increase -> more realistic 2021-11-25 22:53:31 +01:00
Sven Czarnian
3746301b0d use the correct reference time 2021-11-25 22:53:06 +01:00
Sven Czarnian
a8419c286f change the TTL/TTG assignment to the IAF -> predictable shortcuts via DIRECTs 2021-11-25 22:52:48 +01:00
Sven Czarnian
fa0a94d733 use the final assignments 2021-11-25 22:52:09 +01:00
Sven Czarnian
0e96f0402e fix potential crashes 2021-11-25 22:51:39 +01:00
Sven Czarnian
06974b807c fix bugs in TTL and TTG calculations 2021-11-24 12:05:03 +01:00
Sven Czarnian
f359ec8189 add a predicted waypoint for later calculations 2021-11-24 12:04:52 +01:00
Sven Czarnian
be6fe84d77 fix potential crashes 2021-11-24 12:04:25 +01:00
Sven Czarnian
3b767489c3 the conversion to NM is done in the haversine function 2021-11-24 12:04:13 +01:00
Sven Czarnian
5f00ea08cf add the projection function 2021-11-24 12:03:44 +01:00
Sven Czarnian
c767572dee ignore unused return values 2021-11-24 12:03:34 +01:00
Sven Czarnian
bd0fdb0899 fix a crash 2021-11-24 12:03:18 +01:00
Sven Czarnian
21e79a26f0 optimize every minute 2021-11-24 12:03:01 +01:00
Sven Czarnian
693f48c535 send the version, etc 2021-11-22 16:20:46 +01:00
Sven Czarnian
f021baf4cc allow unset weather provider 2021-11-22 16:19:46 +01:00
Sven Czarnian
18577ebe9a move the file for the later distribution 2021-11-18 08:53:53 +01:00
Sven Czarnian
d2b326fa9c add an endpoint to get the airport information 2021-11-18 08:47:09 +01:00
Sven Czarnian
23d00899fc cleanup the imports 2021-11-18 08:46:56 +01:00
Sven Czarnian
74bf3e7439 change to request-response communication for an easier update of euroscope 2021-11-18 08:46:47 +01:00
Sven Czarnian
60d671ea9a do not update the sequence after every optimization 2021-11-18 08:46:03 +01:00
Sven Czarnian
3affbd9d57 add the runway assignment based on shall-should-may 2021-11-17 10:02:29 +01:00
Sven Czarnian
3560e98ad2 set the maximum delay 2021-11-17 10:02:01 +01:00
Sven Czarnian
a9fc6bc701 cleanup the airport configuration 2021-11-17 10:01:49 +01:00
Sven Czarnian
7fe1af1def extend the configuration to define the maximum delay to execute may-assignments 2021-11-17 10:01:35 +01:00
Sven Czarnian
09fdf42255 extend the configuration with the runway assignments 2021-11-16 12:35:18 +01:00
Sven Czarnian
03483953c5 parse the runway assignments 2021-11-16 12:34:46 +01:00
Sven Czarnian
219ff481c3 add the assignment to the runway sequencing 2021-11-16 12:34:17 +01:00
Sven Czarnian
68dbb0b7da add the enumeration for the runway assignments 2021-11-16 12:34:04 +01:00
Sven Czarnian
8426d7cd30 rename the configuration flag 2021-11-15 21:14:31 +01:00
Sven Czarnian
4be95869b0 add the application file to run the complete system 2021-11-15 20:27:43 +01:00
Sven Czarnian
f73a0864de update to the new interface version 2021-11-15 20:27:24 +01:00
Sven Czarnian
0a51874006 update the report to get the latest information 2021-11-15 20:26:05 +01:00
Sven Czarnian
cae904ee39 remove the web URL configuration 2021-11-15 20:14:16 +01:00
Sven Czarnian
9bf0600488 update the configuration code 2021-11-15 18:26:03 +01:00
Sven Czarnian
50cd3e887e add helper functions to update and get information out of the airport data 2021-11-15 18:25:42 +01:00
Sven Czarnian
35d1012bf5 protect the AMAN itself by locks 2021-11-15 18:23:28 +01:00
Sven Czarnian
9028ef0442 remove the web UI 2021-11-15 18:20:33 +01:00
Sven Czarnian
e4ce4ff654 remove acquire() and release() functions and run the threads as deamons for easier cleanups 2021-11-15 17:33:26 +01:00
Sven Czarnian
2d3384f0aa add the REST-API dependencies 2021-11-15 17:29:35 +01:00
Sven Czarnian
7452eb595d request the current configuration after every iteration cycle 2021-11-14 10:02:36 +01:00
Sven Czarnian
f7b8f26e48 add the function to request the configuration 2021-11-14 10:02:21 +01:00
Sven Czarnian
5df1ceb204 the return value is not needed 2021-11-14 10:01:53 +01:00
Sven Czarnian
0a3502e98a add the runway assignment flag 2021-11-14 10:01:22 +01:00
Sven Czarnian
52d0373ebb store the airport's ICAO code 2021-11-14 10:01:13 +01:00
Sven Czarnian
f74ae4900c send the performance data as well 2021-11-14 09:35:51 +01:00
Sven Czarnian
e1663d7742 remove unused files 2021-11-14 08:03:16 +01:00
Sven Czarnian
38b4865ea5 remove useless includes 2021-11-13 22:55:13 +01:00
Sven Czarnian
8b34f622a3 adapt the code to split up predictions form the inbounds 2021-11-13 22:55:04 +01:00
Sven Czarnian
eba9e2deab use the new interfaces to receive the data 2021-11-13 22:54:20 +01:00
Sven Czarnian
22e9018807 fix issues in the data transmission 2021-11-13 22:54:04 +01:00
Sven Czarnian
bf10649df6 add functions to calculate other speeds, etc 2021-11-13 22:53:28 +01:00
Sven Czarnian
b3c98cdcea update the protocol 2021-11-13 22:52:30 +01:00
Sven Czarnian
d3a2784ec6 update to the current version 2021-11-13 15:07:35 +01:00
Sven Czarnian
9631157b10 use the new version 2021-11-13 11:19:16 +01:00
Sven Czarnian
530c9ea731 use the weather model as well 2021-11-13 10:00:54 +01:00
Sven Czarnian
39dcd03458 publish the used wind data as well 2021-11-13 09:57:23 +01:00
Sven Czarnian
46e04fca23 initialize the values with None 2021-11-13 09:56:14 +01:00
Sven Czarnian
7cdb04c8fd remove a useless print command 2021-11-13 09:50:22 +01:00
Sven Czarnian
75224a1952 add more information to the arrival waypoints 2021-11-13 09:46:43 +01:00
Sven Czarnian
8c703c13a1 extend the weather model 2021-11-13 09:46:19 +01:00
Sven Czarnian
e6fc82fd5a reset the timestamp if something went wrong 2021-11-13 09:46:05 +01:00
Sven Czarnian
cf8ec3242e fix bugs in the DWD crawler 2021-11-13 09:45:17 +01:00
Sven Czarnian
cbbaf0c021 change the gafor id to an integer 2021-11-13 09:43:37 +01:00
Sven Czarnian
d6b85b1660 send the sequence to euroscope 2021-11-12 20:14:05 +01:00
Sven Czarnian
f4da74febd initialize the worker 2021-11-12 20:13:53 +01:00
Sven Czarnian
d0be115fce add Euroscope to the worker itself 2021-11-12 20:13:36 +01:00
Sven Czarnian
feb4f85dac split the creation and the acquire-call 2021-11-12 20:12:41 +01:00
Sven Czarnian
d1536804a4 add a function to send the sequence to euroscope 2021-11-12 20:11:53 +01:00
Sven Czarnian
92d992f92c build the new protocol file 2021-11-12 19:53:35 +01:00
Sven Czarnian
07a447136d use the updated communication stack 2021-11-12 19:53:23 +01:00
Sven Czarnian
921919488f calculate the PTA based on linear functions for the complete arrival route 2021-11-12 19:40:19 +01:00
Sven Czarnian
6f36d8f569 run the optimization every time to optimze also based on TTG 2021-11-12 19:38:11 +01:00
Sven Czarnian
40ddd7c188 remove debugging messages 2021-11-12 19:37:26 +01:00
Sven Czarnian
2ef4a485d8 fix issues with the TTG and TTL 2021-11-12 19:37:06 +01:00
Sven Czarnian
63378a347b set the planned arrival route and trackmiles 2021-11-11 23:23:25 +01:00
Sven Czarnian
7088bd7bcd add more planned information 2021-11-11 23:22:51 +01:00
Sven Czarnian
9cd46dc4cc extend the arrival candidates 2021-11-11 23:22:41 +01:00
Sven Czarnian
559ab1fa03 calculate the arrival route 2021-11-11 23:22:15 +01:00
Sven Czarnian
1a23499f61 rename ArrivalTime to ArrivalData due to more non-time-related information in the class 2021-11-11 20:25:20 +01:00
Sven Czarnian
7985dda3ce define the arrival waypoint with time information 2021-11-11 20:22:03 +01:00
Sven Czarnian
98285869cd fix a crash if preceding inbounds exist 2021-11-11 20:21:11 +01:00
Sven Czarnian
f795b301a2 update to current version 2021-11-11 16:34:38 +01:00
Sven Czarnian
7c90ecc3b5 send the sequence to the UI 2021-11-11 14:22:58 +01:00
Sven Czarnian
f162047767 update the fixed sequence inbound tag 2021-11-11 14:17:22 +01:00
Sven Czarnian
094e0c627a add a status if the sequence is fixed 2021-11-11 14:13:21 +01:00
Sven Czarnian
b498fe94d1 serializer not needed 2021-11-11 14:13:13 +01:00
Sven Czarnian
ec019d5006 add the WebUI to the system 2021-11-11 12:57:54 +01:00
Sven Czarnian
7762cbf213 initialize the WebUI 2021-11-11 11:09:59 +01:00
Sven Czarnian
e450b58428 update the interface 2021-11-11 11:06:37 +01:00
Sven Czarnian
014ea5fa0a extend the configuration for the UI communication 2021-11-11 11:05:41 +01:00
Sven Czarnian
9c9e7dd445 reorganize the release-function 2021-11-11 11:05:19 +01:00
Sven Czarnian
5c235f7d2a extend the interface of the windows and allow resequencing 2021-11-11 11:03:41 +01:00
Sven Czarnian
19a9947d3d add a json dumper for the server 2021-11-11 11:03:03 +01:00
Sven Czarnian
3d87c3918b remove some debugging todos 2021-11-10 22:50:00 +01:00
Sven Czarnian
97a2f24f28 use weights to find better sequence with TTG and TTL constraints 2021-11-10 22:45:30 +01:00
Sven Czarnian
dd9e725fc2 fix parts in the runway manager 2021-11-10 22:44:55 +01:00
Sven Czarnian
ccb3774872 calculate the maximum TTL 2021-11-10 22:44:40 +01:00
Sven Czarnian
51c963de52 add a maximum time to lose to define some constraints 2021-11-10 22:44:22 +01:00
Sven Czarnian
7c6d098812 increase computational performance 2021-10-18 18:53:24 +02:00
Sven Czarnian
d191da2303 store the WTC in the inbound itself 2021-10-18 12:42:31 +02:00
Sven Czarnian
12d77d0e71 increase performance 2021-10-18 12:42:18 +02:00
Sven Czarnian
33b32befbc add a comment for the final changes 2021-10-17 17:44:18 +02:00
Sven Czarnian
1b2003e879 update the constructor 2021-10-17 17:44:06 +02:00
Sven Czarnian
7a1d4a5959 optimize the performance 2021-10-17 17:31:29 +02:00
Sven Czarnian
62f2a6c3ed add TODOs for the later release 2021-10-17 17:31:19 +02:00
Sven Czarnian
01ce0f1bfe measure the execution time 2021-10-17 17:28:20 +02:00
Sven Czarnian
6151fc255a remove useless prints 2021-10-17 17:28:10 +02:00
Sven Czarnian
fec26a6d6d call the optimization 2021-10-17 17:12:40 +02:00
Sven Czarnian
061eb7eac6 implement the first version of the ACO algorithm 2021-10-17 17:12:26 +02:00
Sven Czarnian
a468f1cc53 fix a VRB-bug 2021-10-17 17:12:05 +02:00
Sven Czarnian
125eef8729 add some additional parameters 2021-10-17 17:11:58 +02:00
Sven Czarnian
c09a5ffe77 fix some bugs in the runway manager during the ETA calculation 2021-10-16 08:06:04 +02:00
Sven Czarnian
c835944e8d sort the FCFS list 2021-10-15 18:10:01 +02:00
Sven Czarnian
0c97e5aa67 initialize the random number generator 2021-10-15 18:09:43 +02:00
Sven Czarnian
43589eaa35 fix two found crashes 2021-10-14 14:35:27 +02:00
Sven Czarnian
a7541925c7 rename the cost function. it manages more the runways 2021-10-14 14:13:42 +02:00
Sven Czarnian
3b8989508e add the absolut minimal optimization value due RHC definition 2021-10-14 14:12:43 +02:00
Sven Czarnian
217c9ad742 rename the member to PlannedArrivalTime for better readability 2021-10-14 14:12:12 +02:00
Sven Czarnian
91b735df2f calculate times for all possible runways -> allows lookup for later optimization 2021-10-14 10:19:37 +02:00
Sven Czarnian
9f6c9f1ff8 do not double optimize the speed until IAF two times 2021-10-13 21:47:45 +02:00
Sven Czarnian
44837e59f1 extend the logger for debugging purposes 2021-10-13 21:43:12 +02:00
Sven Czarnian
5295d1c155 use the theoretical earliest arrival time 2021-10-13 21:42:45 +02:00
Sven Czarnian
9887aa48a4 add more values for later plannings 2021-10-13 21:42:05 +02:00
Sven Czarnian
73e5a42f52 place the recat spacing in the constraints 2021-10-13 20:10:07 +02:00
Sven Czarnian
3313a8d463 calculate the descend profiles based on the arrival constraints 2021-10-13 18:13:00 +02:00
Sven Czarnian
cea52736bf fix some IAS selection bugs 2021-10-13 18:12:16 +02:00
Sven Czarnian
d388fb5591 add the last waypoint to the constraints 2021-10-13 18:12:04 +02:00
Sven Czarnian
2a83754fa4 fix a conversion bug 2021-10-13 18:11:51 +02:00
Sven Czarnian
a611a91fe3 merge the constraints and the arrival routes 2021-10-13 16:17:10 +02:00
Sven Czarnian
10d06c2f67 parse the constraints and create the waypoint list out of it 2021-10-13 16:16:56 +02:00
Sven Czarnian
17f11a640a create deep copies to allow changes of constraints per arrival 2021-10-13 16:16:32 +02:00
Sven Czarnian
909cfc9e27 redefine the constructor to be more flexible with constraints, etc. 2021-10-13 16:16:03 +02:00
Sven Czarnian
014379740b remove unused variables 2021-10-13 12:52:38 +02:00
Sven Czarnian
1e043e2765 define member variables with capital letters 2021-10-13 12:52:29 +02:00
Sven Czarnian
9d69a60396 introduce classes for the ACO algorithm 2021-10-12 22:30:20 +02:00
Sven Czarnian
7f7506104d extend the inbound 2021-10-12 22:29:51 +02:00
Sven Czarnian
e5a773cdcb introduce the runway 2021-10-12 22:29:15 +02:00
Sven Czarnian
a0b00f7c42 implement the RHC mananger 2021-10-12 22:28:50 +02:00
Sven Czarnian
276e50daa3 extend the configuration 2021-10-12 22:28:35 +02:00
Sven Czarnian
2ef1e13bd6 extend the worker thread 2021-10-12 22:27:55 +02:00
Sven Czarnian
bba4a75527 update the dependencies 2021-10-12 22:27:36 +02:00
Sven Czarnian
58d2f5f7f4 extend the airport configuration 2021-10-12 22:27:17 +02:00
Sven Czarnian
5e6301f749 parse the runways as well 2021-10-12 22:27:02 +02:00
Sven Czarnian
c07e767ef8 code refactoring 2021-10-12 22:26:40 +02:00
Sven Czarnian
e02f429364 add a function to get the correct speeds 2021-10-12 22:26:17 +02:00
Sven Czarnian
36ab891f44 switch to geod-library 2021-10-12 22:26:05 +02:00
Sven Czarnian
a1c48d7851 Merge branch 'feature/weather' into 'develop'
Feature/weather

See merge request nav/aman/aman-sys!2
2021-10-12 20:11:47 +00:00
Sven Czarnian
a86dfa01d8 add a weather model 2021-10-11 21:58:14 +02:00
Sven Czarnian
c6d22d2067 fix the protobuffer version 2021-10-11 21:57:47 +02:00
Sven Czarnian
23add20513 remove wrong imports 2021-10-11 21:57:09 +02:00
Sven Czarnian
fd324ea747 add some logging 2021-10-11 21:56:51 +02:00
Sven Czarnian
ebea408267 make the datetime code more readable 2021-10-11 21:56:39 +02:00
Sven Czarnian
8b43991c50 remove unreachable code 2021-10-10 08:27:19 +02:00
Sven Czarnian
11eae85e35 remove useless checks 2021-10-10 08:27:10 +02:00
Sven Czarnian
7e17bf0103 removed unused files 2021-10-10 08:26:49 +02:00
Sven Czarnian
667829b03d fix a crash during the update call 2021-09-25 09:03:55 +02:00
Sven Czarnian
dc2a435e8e fix a comment 2021-09-24 22:29:28 +02:00
Sven Czarnian
484be00e8c redefined the API to avoid GC issues during the destruction of the AMAN and its children 2021-09-24 22:28:19 +02:00
Sven Czarnian
b69c584fb4 use the weather system in the central AMAN class 2021-09-24 22:11:15 +02:00
Sven Czarnian
af52103ec8 fix an information 2021-09-24 22:10:58 +02:00
Sven Czarnian
11e76a3f24 refactor the code to abstract from the weather provider 2021-09-24 22:10:46 +02:00
Sven Czarnian
b54f7dfc50 fix a copy-paste error 2021-09-05 19:29:21 +02:00
Sven Czarnian
2687f543ad add the weather to the system configuration 2021-09-05 19:27:58 +02:00
Sven Czarnian
3743f31b84 introduce the weather configuration 2021-09-05 19:27:47 +02:00
Sven Czarnian
d851efcd4d add a crawler to parse DWD data 2021-09-05 18:59:30 +02:00
Sven Czarnian
9fd05aa932 fix code formatting 2021-09-05 18:59:16 +02:00
Sven Czarnian
d07751cf77 Merge branch 'feature/setup' into 'develop'
Feature/setup

See merge request nav/aman/aman-sys!1
2021-09-03 21:41:00 +00:00
Sven Czarnian
715433bac6 Merge remote-tracking branch 'origin/develop' into feature/setup
# Conflicts:
#	README.md
2021-09-03 23:39:42 +02:00
Sven Czarnian
9de9b813ba sort the received aircraft reports into the corresponding worker thread 2021-09-03 23:35:17 +02:00
Sven Czarnian
1561335e1b introduce a inbound class to prepare the collection of internal data 2021-09-03 23:34:26 +02:00
Sven Czarnian
b516333ede Merge branch 'feature/setup' of git.vatsim-germany.org:nav/aman-sys into feature/setup 2021-09-03 23:07:14 +02:00
Sven Czarnian
793d92ff83 get the arrival routes 2021-09-03 23:06:26 +02:00
Sven Czarnian
fa38924936 sort the arrival routes by the runways 2021-09-03 23:06:12 +02:00
Sven Czarnian
87d813d0a4 runways are not required. can be extracted out of required arrival routes 2021-09-03 23:05:57 +02:00
Sebastian Kramer
cf191a6ff1 Update .gitignore to reflect cache and build files 2021-09-03 15:38:05 +02:00
Sven Czarnian
aaa37a5f62 execute the planner every minute 2021-09-02 21:01:00 +02:00
Sven Czarnian
744ad71b6c initialize the workers for the airports and destroy them during shutdown 2021-09-02 20:59:41 +02:00
Sven Czarnian
479d7b2d44 add sys to the package definition 2021-09-02 20:59:17 +02:00
Sven Czarnian
64c238899a introduce a worker thread for the planning tasks 2021-09-02 20:59:02 +02:00
Sven Czarnian
518e80e2fe rename System class 2021-09-02 20:37:12 +02:00
Sven Czarnian
46cc87eb3b introduce a wrapper to provide a system class that manages the environment 2021-09-02 19:44:02 +02:00
Sven Czarnian
bd7cbe91ed add a configuration that parses all relevant airport data 2021-09-02 19:43:34 +02:00
Sven Czarnian
f6643d899f remove the obsolete configuration entries 2021-09-02 19:43:18 +02:00
Sven Czarnian
b10fae513e use paths to the keys relative to the configuration path 2021-09-02 19:43:05 +02:00
Sven Czarnian
d60d5cb716 replace exceptions by error messages 2021-09-02 19:35:20 +02:00
Sven Czarnian
b3b5b3e547 rename the configuration names 2021-09-02 19:35:02 +02:00
Sven Czarnian
9c7d8db006 parse the performance data information 2021-09-02 09:32:28 +02:00
Sven Czarnian
8cd5aa6baf introduce a tool that extracts the performance data out of Skybrary 2021-09-02 09:01:16 +02:00
Sven Czarnian
df455df689 introduce a class for performance data 2021-09-02 09:00:53 +02:00
Sven Czarnian
1374ad95c9 add a missing dependency for the crawler tool 2021-09-02 09:00:34 +02:00
Sven Czarnian
31150adb9e add a parser to read SCT/ESE-files 2021-08-30 21:57:56 +02:00
Sven Czarnian
e4715abda3 add some generic datatypes 2021-08-30 21:57:14 +02:00
Sven Czarnian
51b4013e6b fix a crash 2021-08-30 21:56:16 +02:00
Sven Czarnian
f4fbd6245b extend the external dependencies 2021-08-30 21:56:00 +02:00
Sven Czarnian
1d50f0e9af add code to replace the import commands to fix execution errors 2021-08-19 09:18:54 +02:00
Sven Czarnian
8f81b65df8 fux a warning 2021-08-19 09:01:18 +02:00
Sven Czarnian
c7738346bb receive aircraft reports 2021-08-17 17:42:21 +02:00
Sven Czarnian
4e8e8f15e4 update the interface 2021-08-17 17:42:07 +02:00
Sven Czarnian
5a2b9983b6 switch to classic API 2021-08-16 07:58:19 +02:00
Sven Czarnian
36d2bfa8a0 remove the useless destructor 2021-08-15 12:56:42 +02:00
Sven Czarnian
0fdcf8e99e use the destructor 2021-08-15 12:54:18 +02:00
Sven Czarnian
6b2072f43b introduce a receiver thread 2021-08-15 12:54:10 +02:00
Sven Czarnian
04b299730a delete the precompiled python cache 2021-08-15 09:01:39 +02:00
Sven Czarnian
8199b33d53 use one module for the complete ES communication 2021-08-15 09:00:04 +02:00
Sven Czarnian
153930e73c reorder dependencies 2021-08-15 08:59:42 +02:00
Sven Czarnian
7b26e27c9d introduce a configuration module 2021-08-15 08:59:32 +02:00
Sven Czarnian
a3f4f8f41b add argparse as a dependency 2021-08-14 21:31:18 +02:00
Sven Czarnian
59e458c70b add the long description 2021-08-14 21:31:06 +02:00
Sven Czarnian
a0c9676c78 format the setup call 2021-08-14 21:30:49 +02:00
Sven Czarnian
b92f437fcb extend the clean-function 2021-08-14 21:28:53 +02:00
Sven Czarnian
355a5463e5 introduce the initialization code for the aircraft data receiver 2021-08-14 21:28:38 +02:00
Sven Czarnian
d4c07824c6 update the documentation 2021-08-14 21:28:18 +02:00
Sven Czarnian
bbd45778db introduce a tool to create keys for the client and the server 2021-08-14 21:28:04 +02:00
Sven Czarnian
bd2d431c41 define the general package structure 2021-08-14 21:27:45 +02:00
Sven Czarnian
3076821b3a change the output location 2021-08-14 20:08:39 +02:00
Sven Czarnian
0f885c1e00 move the setup file to the correct location 2021-08-14 20:07:09 +02:00
Sven Czarnian
e112ee9694 initial setuptools definition 2021-08-14 19:35:25 +02:00
Sven Czarnian
7d540a9b85 add the protobuf-tool and license 2021-08-14 19:35:13 +02:00
Sven Czarnian
30729676ac add the protobuf-submodule 2021-08-14 19:34:59 +02:00
Sebastian Kramer
c80d230946 Some general considerations. 2021-08-13 10:41:11 +02:00
Sebastian Kramer
a0d4a1e0d3 Create initial tcp server 2021-08-12 10:21:20 +02:00
49 changed files with 3624 additions and 1 deletions

8
.gitignore vendored Normal file
View File

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

4
.gitmodules vendored Normal file
View File

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

View File

@@ -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
View 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
View File

@@ -0,0 +1 @@
0.1.0

0
aman/__init__.py Normal file
View File

230
aman/app.py Normal file
View 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
View 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
View 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
View 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
View File

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env python
import configparser
from aman.types.PerformanceData import PerformanceData
class AircraftPerformance:
def __init__(self, filepath : str):
config = configparser.ConfigParser()
config.read(filepath)
self.Aircrafts = { }
# iterate over all entries
for key in config:
if 'DEFAULT' == key:
continue
aircraft = PerformanceData(key)
aircraft.SpeedAboveFL240 = 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
View 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]

View 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
View 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)

View 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
View 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
View 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
View 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
View File

View 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
View File

View 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)

View 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
View 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
View 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
View File

122
aman/sys/aco/Ant.py Normal file
View 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
View 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

View 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

View 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
View 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

View 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
View File

View 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)

View 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
View File

30
aman/types/ArrivalData.py Normal file
View 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)

View File

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

View 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
View 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']

View 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
View 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
View 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
View File

BIN
external/bin/protoc.exe vendored Normal file

Binary file not shown.

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

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

104
setup.py Normal file
View 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

Submodule src/protobuf added at 893e012b3f