226 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
39 changed files with 2470 additions and 312 deletions

2
.gitmodules vendored
View File

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

View File

@@ -2,7 +2,10 @@
import glob
import os
import random
import sys
from threading import Lock
import time
from aman.com import AircraftReport_pb2
from aman.com.Euroscope import Euroscope
@@ -27,33 +30,32 @@ class AMAN:
def __init__(self):
# default initialization of members
self.systemConfig = None
self.aircraftPerformance = None
self.receiver = None
self.weather = None
self.workers = []
self.inbounds = {}
def __del__(self):
self.release()
def aquire(self):
configPath = AMAN.findConfigPath()
self.SystemConfig = None
self.AircraftPerformance = None
self.Receiver = None
self.Weather = None
self.WebUi = None
self.Workers = []
self.WorkersLock = Lock()
# read all system relevant configuration files
self.systemConfig = System(os.path.join(configPath, 'System.ini'))
self.SystemConfig = System(os.path.join(configPath, 'System.ini'))
print('Parsed System.ini')
# read the aircraft performance data
self.aircraftPerformance = AircraftPerformance(os.path.join(configPath, 'PerformanceData.ini'))
if None == self.aircraftPerformance:
self.AircraftPerformance = AircraftPerformance(os.path.join(configPath, 'PerformanceData.ini'))
if None == self.AircraftPerformance:
sys.stderr.write('No aircraft performance data found!')
sys.exit(-1)
else:
print('Parsed PerformanceData.ini. Extracted ' + str(len(self.aircraftPerformance.aircrafts)) + ' aircrafts')
print('Parsed PerformanceData.ini. Extracted ' + str(len(self.AircraftPerformance.Aircrafts)) + ' aircrafts')
self.weather = Weather()
self.weather.acquire(self.systemConfig.Weather)
# create the communication syb
self.Weather = Weather(self.SystemConfig.Weather)
self.Receiver = Euroscope(configPath, self.SystemConfig.Server, self)
self.acquireLock()
# find the airport configurations and create the workers
airportsPath = os.path.join(os.path.join(configPath, 'airports'), '*.ini')
@@ -64,34 +66,45 @@ class AMAN:
airportConfig = Airport(file, icao)
# initialize the worker thread
worker = Worker()
worker.acquire(icao, airportConfig)
self.workers.append(worker)
worker = Worker(icao, airportConfig, self.Weather, self.AircraftPerformance, self.Receiver)
self.Workers.append(worker)
print('Started worker for ' + icao)
# create the EuroScope receiver
self.receiver = Euroscope()
self.receiver.acquire(configPath, self.systemConfig.Server, self)
self.releaseLock()
def release(self):
if None != self.receiver:
self.receiver.release()
self.receiver = None
# initialize the random number generator
random.seed(time.time())
if None != self.weather:
self.weather.release()
self.weather = None
def acquireLock(self):
if None != self.WorkersLock:
self.WorkersLock.acquire()
if None != self.workers:
for worker in self.workers:
worker.release()
self.workers = None
def releaseLock(self):
if None != self.WorkersLock:
self.WorkersLock.release()
def updateAircraftReport(self, report : AircraftReport_pb2.AircraftReport):
self.acquireLock()
# find the correct worker for the inbound
for worker in self.workers:
if worker.icao == report.destination:
for worker in self.Workers:
if worker.Icao == report.destination:
worker.acquireLock()
worker.reportQueue[report.aircraft.callsign] = report
worker.ReportQueue[report.aircraft.callsign] = report
worker.releaseLock()
break
self.releaseLock()
def findAirport(self, icao : str):
self.acquireLock()
airport = None
for worker in self.Workers:
if icao == worker.Icao:
airport = worker
break
self.releaseLock()
return airport

1
aman/VERSION Normal file
View File

@@ -0,0 +1 @@
0.1.0

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

View File

@@ -19,8 +19,8 @@ from datetime import datetime as dt
# - third element of wind data tuple: wind speed (KT)
class DwdCrawler():
def __init__(self):
self.updateTime = None
self.windData = None
self.UpdateTime = None
self.WindData = None
def parseGaforAreas(areas : str):
areas = areas.replace(':', '')
@@ -57,6 +57,9 @@ class DwdCrawler():
altitude = int(altitude.replace('FL', '')) * 100
else:
altitude = int(altitude.replace('FT', ''))
if 'VRB' == windData[0]:
row = ( altitude, 0, int(windData[1].replace('KT', '')) )
else:
row = ( altitude, int(windData[0]), int(windData[1].replace('KT', '')) )
table.append(row)
@@ -107,7 +110,8 @@ class DwdCrawler():
for line in content.splitlines():
if '' == line:
if 0 != len(windTable):
windInformation.append(( areaIds, windTable ))
for id in areaIds:
windInformation.append([ id, windTable ])
areaIds = None
windTable = []
elif line.startswith('GAFOR-Gebiete'):
@@ -125,8 +129,8 @@ class DwdCrawler():
return nextUpdate, windInformation
def receiveWindData(self):
self.updateTime = None
self.windData = None
self.UpdateTime = None
self.WindData = None
with urllib.request.urlopen('https://www.dwd.de/DE/fachnutzer/luftfahrt/teaser/luftsportberichte/luftsportberichte_node.html') as site:
data = site.read().decode('utf-8')
@@ -141,17 +145,18 @@ class DwdCrawler():
pages.append('https://www.dwd.de/' + link['href'].split(';')[0])
# receive the wind data
self.updateTime = None
self.windData = []
self.UpdateTime = None
self.WindData = {}
for page in pages:
next, wind = self.parseGaforPage(page)
if None != next:
if None == self.updateTime or self.updateTime > next:
self.updateTime = next
self.windData.extend(wind)
if None == self.UpdateTime or self.UpdateTime > next:
self.UpdateTime = next
for gafor in wind:
self.WindData[gafor[0]] = gafor[1]
# indicate that new wind data is available
if None != self.updateTime:
if None != self.UpdateTime:
return True
else:
return False

View File

@@ -1,6 +1,5 @@
#!/usr/bin/env python
import ctypes
import glob
import os
import sys
@@ -9,63 +8,49 @@ import time
import zmq
import zmq.auth
from aman.com import AircraftReport_pb2
from aman.com import Communication_pb2
from aman.config.Server import Server
from threading import Thread, _active
from threading import Thread
class ReceiverThread(Thread):
def __init__(self, socket, aman):
class ComThread(Thread):
def __init__(self, com, aman):
Thread.__init__(self)
self.socket = socket
self.aman = aman
self.Com = com
self.AMAN = aman
def run(self):
while True:
try:
msg = self.socket.recv(zmq.NOBLOCK)
msg = self.Com.Socket.recv(zmq.NOBLOCK)
# parse the received message
report = AircraftReport_pb2.AircraftReport()
report = Communication_pb2.AircraftUpdate()
report.ParseFromString(msg)
# try to associate the received aircraft to an airport
self.aman.updateAircraftReport(report)
# try to associate the received aircrafts to airports
for inbound in report.reports:
self.AMAN.updateAircraftReport(inbound)
# get the sequence of the airport
if None != report.airport:
airport = self.AMAN.findAirport(report.airport)
if None != airport:
self.Com.sendSequence(report.airport, airport.inboundSequence(), airport.WeatherModel)
except zmq.ZMQError as error:
if zmq.EAGAIN == error.errno:
time.sleep(0.5)
time.sleep(0.1)
continue
else:
return
def threadId(self):
if hasattr(self, '_thread_id'):
return self._thread_id
for id, thread in _active.items():
if thread is self:
return id
def stopThread(self):
id = self.threadId()
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(id, ctypes.py_object(SystemExit))
if 1 < res:
ctypes.pythonapi.PyThreadState_SetAsyncExc(id, 0)
# @brief Receives and sends messages to EuroScope plugins
class Euroscope:
def __init__(self):
self.context = None
self.receiverSocket = None
self.receiverThread = None
self.notificationSocket = None
def __del__(self):
self.release()
# @brief Initializes the ZMQ socket
# @param[in] config The server configuration
def acquire(self, configPath : str, config : Server, aman):
self.context = zmq.Context()
def __init__(self, configPath : str, config : Server, aman):
self.Context = None
self.Socket = None
self.Thread = None
self.Context = zmq.Context()
# find the key directories
serverKeyPath = os.path.join(os.path.join(configPath, 'keys'), 'server')
@@ -88,34 +73,62 @@ class Euroscope:
keyPair = zmq.auth.load_certificate(keyPairPath[0])
# initialize the receiver
self.receiverSocket = zmq.Socket(self.context, zmq.SUB)
self.receiverSocket.setsockopt(zmq.CURVE_PUBLICKEY, keyPair[0])
self.receiverSocket.setsockopt(zmq.CURVE_SECRETKEY, keyPair[1])
self.receiverSocket.setsockopt(zmq.CURVE_SERVER, True)
self.receiverSocket.bind('tcp://' + config.Address + ':' + str(config.PortReceiver))
self.receiverSocket.setsockopt(zmq.SUBSCRIBE, b'')
self.receiverThread = ReceiverThread(self.receiverSocket, aman)
self.receiverThread.start()
self.Socket = zmq.Socket(self.Context, zmq.REP)
self.Socket.setsockopt(zmq.CURVE_PUBLICKEY, keyPair[0])
self.Socket.setsockopt(zmq.CURVE_SECRETKEY, keyPair[1])
self.Socket.setsockopt(zmq.CURVE_SERVER, True)
self.Socket.bind('tcp://' + config.Address + ':' + str(config.PortReceiver))
#self.Socket.setsockopt(zmq.SUBSCRIBE, b'')
self.Thread = ComThread(self, aman)
self.Thread.setDaemon(True)
self.Thread.start()
print('Listening to tcp://' + config.Address + ':' + str(config.PortReceiver))
# initialize the notification
self.notificationSocket = zmq.Socket(self.context, zmq.PUB)
self.notificationSocket.setsockopt(zmq.CURVE_PUBLICKEY, keyPair[0])
self.notificationSocket.setsockopt(zmq.CURVE_SECRETKEY, keyPair[1])
self.notificationSocket.setsockopt(zmq.CURVE_SERVER, True)
self.notificationSocket.bind('tcp://' + config.Address + ':' + str(config.PortNotification))
print('Publishing to tcp://' + config.Address + ':' + str(config.PortNotification))
def sendSequence(self, airport : str, inbounds, weather):
if None == self.Socket:
return
def release(self):
if None != self.receiverThread:
self.receiverThread.stopThread()
self.receiverThread.join()
self.receiverThread = None
sequence = Communication_pb2.AircraftSequence()
sequence.airport = airport
if None != self.receiverSocket:
self.receiverSocket.close()
self.receiverSocket = None
# convert the wind data
if None != weather.Altitudes:
for i in range(0, len(weather.Altitudes)):
entry = sequence.windData.add()
entry.altitude = int(weather.Altitudes[i])
entry.direction = int(weather.Directions[i])
entry.speed = int(weather.Windspeeds[i])
if None != self.notificationSocket:
self.notificationSocket.close()
self.notificationSocket = None
# convert the inbound sequence
for inbound in inbounds:
entry = sequence.sequence.add()
entry.callsign = inbound.Callsign
entry.fixed = inbound.FixedSequence
if None != inbound.PlannedStar:
entry.arrivalRoute = inbound.PlannedStar.Name
if None != inbound.PlannedRunway:
entry.arrivalRunway = inbound.PlannedRunway.Name
if None != inbound.PerformanceData:
entry.performance.iasAboveFL240 = int(round(inbound.PerformanceData.SpeedAboveFL240))
entry.performance.iasAboveFL100 = int(round(inbound.PerformanceData.SpeedAboveFL100))
entry.performance.iasBelowFL100 = int(round(inbound.PerformanceData.SpeedBelowFL100))
entry.performance.iasApproach = int(round(inbound.PerformanceData.SpeedApproach))
if None != inbound.PlannedArrivalRoute:
for waypoint in inbound.PlannedArrivalRoute:
wp = entry.waypoints.add()
wp.name = waypoint.Waypoint.Name
wp.altitude = int(round(waypoint.Altitude))
wp.indicatedAirspeed = int(round(waypoint.IndicatedAirspeed))
wp.groundSpeed = int(round(waypoint.GroundSpeed))
pta = str(waypoint.PTA)
delimiter = pta.find('.')
if -1 == delimiter:
delimiter = pta.find('+')
wp.pta = pta[0:delimiter]
message = sequence.SerializeToString()
self.Socket.send(message)

View File

@@ -11,46 +11,37 @@ from aman.com.DwdCrawler import DwdCrawler
import aman.config.Weather
class Weather(Thread):
def __init__(self):
def __init__(self, config : aman.config.Weather.Weather):
Thread.__init__(self)
self.nextUpdate = None
self.lastUpdateTried = None
self.stopThread = False
self.provider = None
def acquire(self, config : aman.config.Weather.Weather):
self.nextUpdate = dt.utcfromtimestamp(int(time.time()))
self.lastUpdateTried = None
self.stopThread = False
self.provider = None
self.NextUpdate = dt.utcfromtimestamp(int(time.time()))
self.LastUpdateTried = None
self.StopThread = False
self.Provider = None
if 'DWD' == config.Provider.upper():
self.provider = DwdCrawler()
else:
self.Provider = DwdCrawler()
elif 'NONE' != config.Provider.upper():
sys.stderr.write('Invalid or unknown weather-provider defined')
sys.exit(-1)
self.setDaemon(True)
self.start()
def release(self):
self.stopThread = True
self.join()
def currentClock():
clock = dt.utcfromtimestamp(int(time.time())).replace(tzinfo = pytz.UTC)
return clock
def run(self):
while False == self.stopThread and None != self.provider:
while False == self.StopThread and None != self.Provider:
now = Weather.currentClock()
# check if an update is required
if None != self.provider.updateTime and self.provider.updateTime > now:
if None != self.Provider.UpdateTime and self.Provider.UpdateTime > now:
time.sleep(1)
continue
if None == self.lastUpdateTried or self.lastUpdateTried <= now:
if True == self.provider.receiveWindData():
self.nextUpdate = self.provider.updateTime
if None == self.LastUpdateTried or self.LastUpdateTried <= now:
if True == self.Provider.receiveWindData():
self.NextUpdate = self.Provider.UpdateTime
print('Received new wind data')

View File

@@ -8,7 +8,7 @@ class AircraftPerformance:
def __init__(self, filepath : str):
config = configparser.ConfigParser()
config.read(filepath)
self.aircrafts = { }
self.Aircrafts = { }
# iterate over all entries
for key in config:
@@ -17,12 +17,12 @@ class AircraftPerformance:
aircraft = PerformanceData(key)
aircraft.speedAboveFL240 = config[key]['speedabovefl240']
aircraft.rodAboveFL240 = config[key]['rodabovefl240']
aircraft.speedAboveFL100 = config[key]['speedabovefl100']
aircraft.rodAboveFL100 = config[key]['rodabovefl100']
aircraft.speedBelowFL100 = config[key]['speedbelowfl100']
aircraft.rodBelowFL100 = config[key]['rodbelowfl100']
aircraft.speedApproach = config[key]['speedapproach']
aircraft.SpeedAboveFL240 = float(config[key]['speedabovefl240'])
aircraft.RodAboveFL240 = float(config[key]['rodabovefl240'])
aircraft.SpeedAboveFL100 = float(config[key]['speedabovefl100'])
aircraft.RodAboveFL100 = float(config[key]['rodabovefl100'])
aircraft.SpeedBelowFL100 = float(config[key]['speedbelowfl100'])
aircraft.RodBelowFL100 = float(config[key]['rodbelowfl100'])
aircraft.SpeedApproach = float(config[key]['speedapproach'])
self.aircrafts[aircraft.icao] = aircraft
self.Aircrafts[aircraft.Icao] = aircraft

View File

@@ -1,11 +1,16 @@
#!/usr/bin/env python
import configparser
from datetime import timedelta
import glob
import os
import sys
from formats.SctEseFormat import SctEseFormat
from aman.config.RHC import RHC
from aman.config.AirportSequencing import AirportSequencing
from aman.config.RunwaySequencing import RunwaySequencing, RunwayAssignmentType
from aman.formats.SctEseFormat import SctEseFormat
from aman.types.Waypoint import Waypoint
class Airport:
def findGngData(data, path):
@@ -30,14 +35,282 @@ class Airport:
return []
return planning['routes'].split(':')
def __init__(self, filepath : str, icao : str):
self.arrivalRoutes = {}
def parseDefaultSequencingConfiguration(self, icao : str, planning):
if None == planning.get('activearrivalrunwaydefault'):
sys.stderr.write('No "activearrivalrunwaydefault" entry found!')
sys.exit(-1)
if None == planning.get('activearrivalmodedefault'):
sys.stderr.write('No "activearrivalmodedefault" entry found!')
sys.exit(-1)
if None == planning.get('arrivalspacingdefault'):
sys.stderr.write('No "arrivalspacingdefault" entry found!')
sys.exit(-1)
if not icao in self.GngData.Runways:
sys.stderr.write('Unable to find' + icao + 'in the SCT data!')
sys.exit(-1)
# parse the default arrival mode
if 'STAGGERED' == planning['activearrivalmodedefault']:
staggered = True
elif 'IPA' == planning['activearrivalmodedefault']:
staggered = False
else:
sys.stderr.write('Unknown arrival mode in "" found! (STAGGERED or IPA needs to be set)')
sys.exit(-1)
# translate the spacing into a map
ident = ''
spacings = {}
spacingConfig = list(filter(None, planning['arrivalspacingdefault'].split(':')))
for i in range(0, len(spacingConfig)):
if 0 == i % 2:
ident = spacingConfig[i]
elif '' != ident:
spacings[ident] = int(spacingConfig[i])
else:
sys.stderr.write('No runway defined in "arrivalspacingdefault"!')
sys.exit(-1)
# create the sequencing data per runway
self.DefaultSequencing = AirportSequencing(icao)
for ident in list(filter(None, planning['activearrivalrunwaydefault'].split(':'))):
if not ident in spacings:
sys.stderr.write('Unable to find sequencing data for ' + ident + ' of ' + icao)
sys.exit(-1)
found = False
for runway in self.GngData.Runways[icao]:
if ident == runway.Name:
sequence = RunwaySequencing(runway)
sequence.Spacing = spacings[ident]
self.DefaultSequencing.activateRunway(sequence)
found = True
break
if False == found:
sys.stderr.write('Unable to find the runway for ' + ident + ' of ' + icao + ' in SCT data!')
sys.exit(-1)
# create the dependencies, if needed
if True == staggered:
if None == planning.get('runwaydependenciesdefault'):
sys.stderr.write('Unable to find the runway dependencies for staggered approaches of ' + icao + '!')
sys.exit(-1)
dependencies = list(filter(None, planning['runwaydependenciesdefault'].split(':')))
if 0 != len(dependencies) % 2:
sys.stderr.write('No valid set of runway dependencies found!')
sys.exit(-1)
for i in range(0, len(dependencies), 2):
self.DefaultSequencing.addDependency(dependencies[i], dependencies[i + 1])
def parseConstraints(self, planning):
self.ArrivalRouteConstraints = {}
# check if the IAF sequence constraint is defined
if 'iafsequence' in planning:
self.IafSpacing = float(planning['iafsequence'])
else:
self.IafSpacing = 10.0
# parse the arrival constraints
for key in planning:
if True == key.startswith('constraints'):
star = key.replace('constraints', '').upper()
if '' != star:
elements = list(filter(None, planning[key].split(':')))
if 3 > len(elements):
sys.stderr.write('Invalid constraint line: ' + key + '=' + planning[key])
sys.exit(-1)
waypoints = []
# values for the waypoint constraints
waypointName = elements[0]
constraints = [-1, -1]
isBaseTurn = False
isFinalTurn = False
index = 1
while index < len(elements):
if 'A' == elements[index] or 'S' == elements[index]:
if index + 1 == len(elements) or False == elements[index + 1].isnumeric():
sys.stderr.write('Invalid constraint line: ' + key + '=' + planning[key])
sys.exit(-1)
if 'A' == elements[index]:
constraints[0] = int(elements[index + 1])
else:
constraints[1] = int(elements[index + 1])
index += 1
elif 'B' == elements[index]:
isBaseTurn = True
elif 'F' == elements[index]:
isFinalTurn = True
else:
if False == isBaseTurn and False == isFinalTurn and -1 == constraints[0] and -1 == constraints[1] and '' == waypointName:
sys.stderr.write('Invalid constraint line: ' + key + '=' + planning[key])
sys.exit(-1)
if True == isBaseTurn and True == isFinalTurn:
sys.stderr.write('Invalid constraint line: ' + key + '=' + planning[key])
sys.exit(-1)
waypoints.append(Waypoint(name = waypointName, base = isBaseTurn, final = isFinalTurn))
if -1 != constraints[0]:
waypoints[-1].Altitude = constraints[0]
if -1 != constraints[1]:
waypoints[-1].Speed = constraints[1]
# reset temporary data
waypointName = elements[index]
constraints = [-1, -1]
isBaseTurn = False
isFinalTurn = False
index += 1
# check if we have to add the last waypoint
if 0 != len(waypoints) and waypointName != waypoints[-1].Name:
waypoints.append(Waypoint(name = waypointName, base = isBaseTurn, final = isFinalTurn))
if -1 != constraints[0]:
waypoints[-1].Altitude = constraints[0]
if -1 != constraints[1]:
waypoints[-1].Speed = constraints[1]
# register the arrival route
self.ArrivalRouteConstraints[star] = waypoints
def parseAssignment(assignment : str):
elements = list(filter(None, assignment.split(':')))
retval = {}
type = None
index = 0
while index < len(elements):
if 0 == index % 2:
if 'A' == elements[index]:
type = RunwayAssignmentType.AircraftType
elif 'G' == elements[index]:
type = RunwayAssignmentType.GateAssignment
else:
sys.stderr.write('Invalid assignment type: ' + elements[index])
sys.exit(-1)
else:
if None == type:
sys.stderr.write('No assignment type defined')
sys.exit(-1)
if type not in retval:
retval.setdefault(type, [])
retval[type].append(elements[index])
type = None
index += 1
return retval
def findRunway(self, icao : str, name : str):
for runway in self.GngData.Runways[icao]:
if name == runway.Name:
return runway
sys.stderr.write('Unable to find runway ' + name + ' in the sequencing data for ' + icao)
raise Exception()
def updateRunwayAssignment(dictionary, runway, assignments):
if runway not in dictionary:
dictionary.setdefault(runway, {})
for key in assignments:
if key not in dictionary[runway]:
dictionary[runway].setdefault(key, assignments[key])
else:
dictionary[runway][key].extend(assignments[key])
def parseOptimization(self, key : str, line : str):
star = key.replace('optimization', '').upper()
# check if the STAR exists
found = False
for rwy in self.GngData.ArrivalRoutes:
for route in self.GngData.ArrivalRoutes[rwy]:
if star == route.Name:
found = True
break
if True == found:
break
if False == found:
sys.stderr.write('Unknown star:' + key)
raise Exception()
elements = line.split(':')
if 2 != len(elements):
sys.stderr.write('Invalid optimization parameter for ' + key)
raise Exception()
maxTTG = int(elements[0])
ttgRatio = float(elements[1])
return star, maxTTG, ttgRatio
def updateOptimizationParameters(dictionary, star, maxTTG, ttgRatio):
if star not in dictionary:
dictionary.setdefault(star, [])
dictionary[star] = [ maxTTG, ttgRatio ]
def parseRunwayAssignment(self, icao : str, planning):
self.OptimizationParameters = {}
self.RunwayAssignmentsShall = {}
self.RunwayAssignmentsShould = {}
self.RunwayAssignmentsMay = {}
self.MaxDelayMay = timedelta(minutes=10)
mayFound = False
for key in planning:
if True == key.startswith('shallassign'):
runway = self.findRunway(icao, key.replace('shallassign', '').upper())
assignments = Airport.parseAssignment(planning[key])
Airport.updateRunwayAssignment(self.RunwayAssignmentsShall, runway, assignments)
elif True == key.startswith('shouldassign'):
runway = self.findRunway(icao, key.replace('shouldassign', '').upper())
assignments = Airport.parseAssignment(planning[key])
Airport.updateRunwayAssignment(self.RunwayAssignmentsShould, runway, assignments)
elif True == key.startswith('mayassign'):
runway = self.findRunway(icao, key.replace('mayassign', '').upper())
assignments = Airport.parseAssignment(planning[key])
Airport.updateRunwayAssignment(self.RunwayAssignmentsMay, runway, assignments)
mayFound = True
elif True == key.startswith('optimization'):
star, maxTTG, ttgRatio = self.parseOptimization(key, planning[key])
Airport.updateOptimizationParameters(self.OptimizationParameters, star, maxTTG, ttgRatio)
# find the max delays
if True == mayFound:
if 'maxdelaymay' not in planning:
sys.stderr.write('maxDelaymay needs to be defined')
sys.exit(-1)
self.MaxDelayMay = timedelta(minutes=int(planning['maxdelaymay']))
def parseWebUI(self, webui):
self.IafColorization = {}
for key in webui:
if 'iafcolorization' == key:
elements = list(filter(None, webui[key].split(':')))
for i in range(0, len(elements), 4):
self.IafColorization[elements[i]] = [ int(elements[i + 1]), int(elements[i + 2]), int(elements[i + 3]) ]
def __init__(self, filepath : str, icao : str):
config = configparser.ConfigParser()
config.read(filepath)
dataConfig = None
planningConfig = None
rhcConfig = None
webUiConfig = None
# search the required sections
for key in config:
@@ -45,6 +318,10 @@ class Airport:
dataConfig = config['DATA']
elif 'PLANNING' == key:
planningConfig = config['PLANNING']
elif 'RHC' == key:
rhcConfig = config['RHC']
elif 'WEBUI' == key:
webUiConfig = config['WEBUI']
# find the GNG-file data
sctFile, eseFile = Airport.findGngData(dataConfig, os.path.dirname(filepath))
@@ -61,6 +338,40 @@ class Airport:
sys.stderr.write('No valid planning configuration found')
sys.exit(-1)
# parse the RHC information
if None == rhcConfig:
sys.stderr.write('No RHC configuration found')
sys.exit(-1)
self.RecedingHorizonControl = RHC(rhcConfig)
# check if thw WebUI information is available
if None == webUiConfig:
sys.stderr.write('No WEBUI configuration found')
sys.exit(-1)
# parse the GNG data
print('Used GNG-Data: ' + eseFile)
self.gngData = SctEseFormat(sctFile, eseFile, icao, requiredArrivalRoutes)
self.GngData = SctEseFormat(sctFile, eseFile, icao, requiredArrivalRoutes)
# get the GAFOR id
if None == dataConfig.get('gaforid'):
sys.stderr.write('No GAFOR-ID found!')
sys.exit(-1)
self.GaforId = int(dataConfig['gaforid'])
# get the default sequencing data
self.parseDefaultSequencingConfiguration(icao, planningConfig)
self.parseConstraints(planningConfig)
self.parseRunwayAssignment(icao, planningConfig)
self.parseWebUI(webUiConfig)
self.assignmentUpdate(self.DefaultSequencing)
def assignmentUpdate(self, sequenceConfig : AirportSequencing):
# initializes the default sequence data
for active in sequenceConfig.ActiveArrivalRunways:
if active.Runway in self.RunwayAssignmentsShall:
active.ShallAssignments = self.RunwayAssignmentsShall[active.Runway]
if active.Runway in self.RunwayAssignmentsShould:
active.ShouldAssignments = self.RunwayAssignmentsShould[active.Runway]
if active.Runway in self.RunwayAssignmentsMay:
active.MayAssignments = self.RunwayAssignmentsMay[active.Runway]

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 = {}

View File

@@ -8,6 +8,9 @@ class Server():
self.Address = None
self.PortReceiver = None
self.PortNotification = None
self.WebUiUrl = None
self.WebUiSequenceNotification = None
self.WebUiConfigurationReceiver = None
# search the required sections
for key in config:

View File

@@ -6,8 +6,6 @@ import sys
class Weather():
def __init__(self, config : configparser.ConfigParser):
self.Provider = None
self.PortReceiver = None
self.PortNotification = None
# search the required sections
for key in config:

View File

@@ -1,8 +1,10 @@
#!/usr/bin/env python
import copy
import sys
from aman.types.ArrivalRoute import ArrivalRoute
from aman.types.Runway import Runway
from aman.types.Waypoint import Waypoint
class SctEseFormat:
@@ -30,11 +32,22 @@ class SctEseFormat:
if len(split) <= longitudeIdx:
sys.stderr.write('Invalid waypoint format: ' + waypoint)
sys.exit(-1)
return Waypoint(split[nameIdx], Waypoint.dms2dd(split[latitudeIdx]), Waypoint.dms2dd(split[longitudeIdx]))
return Waypoint(name = split[nameIdx], latitude = split[latitudeIdx], longitude = split[longitudeIdx])
def extractWaypoints(self, sctFilepath : str):
def parseRunway(runway : str):
split = list(filter(None, runway.split(' ')))
if 9 != len(split) or '' == split[8]:
return None, None, None
waypoint0 = Waypoint(name = split[0], latitude = split[4], longitude = split[5])
waypoint1 = Waypoint(name = split[1], latitude = split[6], longitude = split[7])
return split[8], Runway(waypoint0, waypoint1), Runway(waypoint1, waypoint0)
def extractSctInformation(self, sctFilepath : str):
config = SctEseFormat.readFile(sctFilepath)
foundAirports = False
foundRunways = False
foundVOR = False
foundNDB = False
foundFix = False
@@ -48,6 +61,8 @@ class SctEseFormat:
foundFix = True
elif 'AIRPORT' == key:
foundAirports = True
elif 'RUNWAY' == key:
foundRunways = True
if False == foundVOR:
sys.stderr.write('Unable to find VOR-entries in the sector file')
@@ -61,27 +76,39 @@ class SctEseFormat:
if False == foundAirports:
sys.stderr.write('Unable to find AIRPORT-entries in the sector file')
sys.exit(-1)
if False == foundRunways:
sys.stderr.write('Unable to find RUNWAY-entries in the sector file')
sys.exit(-1)
# extract all waypoints
for waypoint in config['VOR']:
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 2, 3)
self.waypoints.setdefault(waypoint.name, []).append(waypoint)
self.Waypoints.setdefault(waypoint.Name, []).append(waypoint)
for waypoint in config['NDB']:
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 1, 2)
self.waypoints.setdefault(waypoint.name, []).append(waypoint)
self.Waypoints.setdefault(waypoint.Name, []).append(waypoint)
for waypoint in config['FIXES']:
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 1, 2)
self.waypoints.setdefault(waypoint.name, []).append(waypoint)
self.Waypoints.setdefault(waypoint.Name, []).append(waypoint)
# extract the airports
for airport in config['AIRPORT']:
airport = SctEseFormat.parseWaypoint(airport, 0, 2, 3)
self.airports.setdefault(airport.name, []).append(airport)
self.Airports.setdefault(airport.Name, []).append(airport)
# extract the runways
for runway in config['RUNWAY']:
airport, runway0, runway1 = SctEseFormat.parseRunway(runway)
if None != airport:
if not airport in self.Runways:
self.Runways.setdefault(airport, [])
self.Runways[airport].append(runway0)
self.Runways[airport].append(runway1)
def parseArrivalRoute(self, route : str, airport : Waypoint):
# split the route and validate that it is a STAR for the airport
split = route.split(':')
if 5 != len(split) or 'STAR' != split[0] or split[1] != airport.name:
if 5 != len(split) or 'STAR' != split[0] or split[1] != airport.Name:
return None
# find all waypoints
@@ -89,7 +116,7 @@ class SctEseFormat:
route = list(filter(None, split[4].split(' ')))
for waypoint in route:
# find the waypoint in the route
coordinates = self.waypoints[waypoint]
coordinates = self.Waypoints[waypoint]
# no waypoint with this name defined
if None == coordinates:
sys.stderr.write('Unable to find waypoint ' + waypoint)
@@ -111,10 +138,10 @@ class SctEseFormat:
sys.stderr.write('Unable to find a close waypoint for ' + waypoint)
sys.exit(-1)
waypoints.append(nearest)
waypoints.append(copy.deepcopy(nearest))
# extend the list of waypoints
else:
waypoints.append(coordinates[0])
waypoints.append(copy.deepcopy(coordinates[0]))
# create the arrival route
return ArrivalRoute(split[3], split[2], waypoints)
@@ -124,10 +151,10 @@ class SctEseFormat:
foundSidsStars = False
# search the airport in the extracted list
if not airport in self.airports:
sys.stderr.write(airport + 'in self.airports', 'Unable to find the requested airport')
if not airport in self.Airports:
sys.stderr.write('Unable to find the requested airport')
sys.exit(-1)
airport = self.airports[airport][0]
airport = self.Airports[airport][0]
for key in config:
if 'SIDSSTARS' == key:
@@ -140,13 +167,14 @@ class SctEseFormat:
# parse all arrival routes
for line in config['SIDSSTARS']:
route = self.parseArrivalRoute(line, airport)
if None != route and route.name in allowedRoutes:
self.arrivalRoutes.setdefault(route.runway, []).append(route)
if None != route and route.Name in allowedRoutes:
self.ArrivalRoutes.setdefault(route.Runway, []).append(route)
def __init__(self, sctFilepath : str, eseFilepath : str, airport : str, allowedRoutes : list):
self.arrivalRoutes = {}
self.waypoints = {}
self.airports = {}
self.ArrivalRoutes = {}
self.Waypoints = {}
self.Airports = {}
self.Runways = {}
self.extractWaypoints(sctFilepath)
self.extractSctInformation(sctFilepath)
self.extractArrivalRoutes(eseFilepath, airport, allowedRoutes)

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

View File

@@ -7,13 +7,16 @@ import scipy.interpolate
class WeatherModel:
def __init__(self, gaforId, weather : Weather):
self.gafor = gaforId
self.weather = weather
self.windDirectionModel = None
self.windSpeedModel = None
self.lastWeatherUpdate = None
self.minimumAltitude = 1000000
self.maximumAltitude = -1
self.Gafor = gaforId
self.Weather = weather
self.Altitudes = None
self.Directions = None
self.Windspeeds = None
self.WindDirectionModel = None
self.WindSpeedModel = None
self.LastWeatherUpdate = None
self.MinimumAltitude = 1000000
self.MaximumAltitude = -1
# create the density interpolation model
# the density model is based on https://aerotoolbox.com/atmcalc/
@@ -95,54 +98,87 @@ class WeatherModel:
return ias * math.sqrt(1.225 / self.densityModel(altitude).item())
def updateWindModel(self):
if None == self.lastWeatherUpdate or self.lastWeatherUpdate != self.weather.provider.updateTime:
self.lastWeatherUpdate = self.weather.provider.updateTime
if None == self.Weather or None == self.Weather.Provider:
return
self.minimumAltitude = 1000000
self.maximumAltitude = -1
self.windDirectionModel = None
self.windSpeedModel = None
if None == self.LastWeatherUpdate or self.LastWeatherUpdate != self.Weather.Provider.UpdateTime:
self.MinimumAltitude = 1000000
self.MaximumAltitude = -1
self.WindDirectionModel = None
self.WindSpeedModel = None
self.Altitudes = None
self.Directions = None
self.Windspeeds = None
if None != self.weather.provider.windData and self.gafor in self.weather.provider.windData:
altitudes = []
directions = []
speeds = []
if None != self.Weather.Provider.WindData and self.Gafor in self.Weather.Provider.WindData:
self.Altitudes = []
self.Directions = []
self.Windspeeds = []
# collect the data for the wind model
for level in self.weather.provider.windData[self.gafor]:
altitudes.append(level[0])
directions.append(level[1])
speeds.append(level[2])
for level in self.Weather.Provider.WindData[self.Gafor]:
self.Altitudes.append(level[0])
self.Directions.append(level[1])
self.Windspeeds.append(level[2])
# define the thresholds for later boundary checks
if self.minimumAltitude > level[0]:
self.minimumAltitude = level[0]
if self.maximumAltitude < level[0]:
self.maximumAltitude = level[0]
if self.MinimumAltitude > level[0]:
self.MinimumAltitude = level[0]
if self.MaximumAltitude < level[0]:
self.MaximumAltitude = level[0]
# calculate the models
if 1 < len(altitudes):
self.windDirectionModel = scipy.interpolate.interp1d(altitudes, directions)
self.windSpeedModel = scipy.interpolate.interp1d(altitudes, speeds)
if 1 < len(self.Altitudes):
self.WindDirectionModel = scipy.interpolate.interp1d(self.Altitudes, self.Directions)
self.WindSpeedModel = scipy.interpolate.interp1d(self.Altitudes, self.Windspeeds)
self.LastWeatherUpdate = self.Weather.Provider.UpdateTime
else:
self.LastWeatherUpdate = None
def calculateGS(self, altitude : int, ias : int, heading : int):
def interpolateWindData(self, altitude : int):
self.updateWindModel()
tas = self.calculateTAS(altitude, ias)
# initialize the wind data
if None != self.windDirectionModel and None != self.windSpeedModel:
# initialized the wind data
if None != self.WindDirectionModel and None != self.WindSpeedModel:
direction = 0.0
speed = 0.0
if None != self.windSpeedModel and None != self.windDirectionModel:
if self.maximumAltitude <= altitude:
altitude = self.maximumAltitude - 1
if self.minimumAltitude >= altitude:
altitude = self.minimumAltitude + 1
direction = self.windDirectionModel(altitude).item()
speed = self.windSpeedModel(altitude).item()
if None != self.WindSpeedModel and None != self.WindDirectionModel:
if self.MaximumAltitude <= altitude:
altitude = self.MaximumAltitude - 1
if self.MinimumAltitude >= altitude:
altitude = self.MinimumAltitude + 1
direction = self.WindDirectionModel(altitude).item()
speed = self.WindSpeedModel(altitude).item()
else:
speed = 0
direction = 0
# calculate the ground speed based on the headwind component
return speed, direction
def calculateGS(self, altitude : int, ias : int, heading : int):
speed, direction = self.interpolateWindData(altitude)
tas = self.calculateTAS(altitude, ias)
return tas + speed * math.cos(math.radians(direction) - math.radians(heading))
def convertGSToTAS(self, altitude : int, gs : int, heading : int):
speed, direction = self.interpolateWindData(altitude)
return gs - speed * math.cos(math.radians(direction) - math.radians(heading))
def estimateCourse(self, altitude : int, gs : int, heading : int):
tas = self.convertGSToTAS(altitude, gs, heading)
speed, direction = self.interpolateWindData(altitude)
aca = heading - direction
wca = speed * aca / tas
if 0 <= aca:
course = heading + wca
else:
course = heading - wca
while 0 > course:
course += 360
while 360 < course:
course -= 360
return course

View File

@@ -1,53 +1,140 @@
#!/usr/bin/env python
from threading import Thread, Lock
import sys
import time
from aman.com import Weather
from aman.com.Euroscope import Euroscope
from aman.config.Airport import Airport
from aman.config.AirportSequencing import AirportSequencing
from aman.sys.aco.Colony import Colony
from aman.sys.aco.Configuration import Configuration
from aman.sys.aco.Node import Node
from aman.sys.WeatherModel import WeatherModel
from aman.sys.RecedingHorizonControl import RecedingHorizonControl
from aman.types.Inbound import Inbound
from aman.types.PerformanceData import PerformanceData
class Worker(Thread):
def __init__(self):
def __init__(self, icao : str, configuration : Airport, weather : Weather,
performance : PerformanceData, euroscope : Euroscope):
Thread.__init__(self)
self.stopThread = None
self.icao = None
self.configuration = None
self.arrivalRoutes = None
self.updateLock = None
self.reportQueue = {}
self.StopThread = None
self.Icao = icao
self.Configuration = configuration
self.SequencingConfiguration = configuration.DefaultSequencing
self.PerformanceData = performance
self.UpdateLock = Lock()
self.ReportQueue = {}
if None != weather:
self.WeatherModel = WeatherModel(configuration.GaforId, weather)
else:
self.WeatherModel = WeatherModel(0, None)
self.RecedingHorizonControl = RecedingHorizonControl(configuration.RecedingHorizonControl)
self.Euroscope = euroscope
def __del__(self):
self.release()
# merge the constraint information with the GNG information
for runway in self.Configuration.GngData.ArrivalRoutes:
for star in self.Configuration.GngData.ArrivalRoutes[runway]:
for name in self.Configuration.ArrivalRouteConstraints:
if name == star.Name:
for constraint in self.Configuration.ArrivalRouteConstraints[name]:
foundWaypoint = False
def acquire(self, icao : str, configuration : Airport):
self.stopThread = None
self.icao = icao
self.configuration = configuration
self.arrivalRoutes = configuration.gngData.arrivalRoutes
self.updateLock = Lock()
self.reportQueue = {}
for waypoint in star.Route:
if constraint.Name == waypoint.Name:
waypoint.Altitude = constraint.Altitude
waypoint.Speed = constraint.Speed
waypoint.BaseTurn = constraint.BaseTurn
waypoint.FinalTurn = constraint.FinalTurn
foundWaypoint = True
break
if False == foundWaypoint:
sys.stderr.write('Unable to find ' + constraint.Name + ' in ' + name)
sys.exit(-1)
break
self.setDaemon(True)
self.start()
def acquireLock(self):
if None != self.updateLock:
self.updateLock.acquire()
def release(self):
self.stopThread = True
self.join()
if None != self.UpdateLock:
self.UpdateLock.acquire()
def releaseLock(self):
if None != self.updateLock:
self.updateLock.release()
if None != self.UpdateLock:
self.UpdateLock.release()
def run(self):
counter = 0
while None == self.stopThread:
while None == self.StopThread:
time.sleep(1)
counter += 1
if 0 != (counter % 60):
continue
# TODO handle the report queue and update internal information
# TODO execute planning, etc.
continue
self.acquireLock()
# perform some book-keeping
self.RecedingHorizonControl.cleanupWindows()
# update the aircraft information in RHC
for callsign in self.ReportQueue:
report = self.ReportQueue[callsign]
if '' != report.initialApproachFix:
inbound = Inbound(report, self.PerformanceData)
Node(inbound, inbound.ReportTime, self.WeatherModel, self.Configuration, self.SequencingConfiguration)
if None != inbound.EnrouteArrivalTime:
self.RecedingHorizonControl.updateReport(inbound)
else:
print('Unable to find all data of ' + report.aircraft.callsign)
self.ReportQueue.clear()
# search the ACO relevant aircrafts
relevantInbounds, earliestArrivalTime = self.RecedingHorizonControl.optimizationRelevantInbounds()
if None != relevantInbounds:
start = time.process_time()
# get the last landing aircrafts per runway before the RHC stage to check for constraints
# this is required to handle the overlap between windows
runways, iafs = self.RecedingHorizonControl.latestFixedInbounds(self.Configuration, self.SequencingConfiguration)
# configure the ACO run
acoConfig = Configuration(constraints = self.SequencingConfiguration, config = self.Configuration,
earliest = earliestArrivalTime, weather = self.WeatherModel,
preceedingRunways = runways, preceedingIafs = iafs,
ants = 5 * len(relevantInbounds), generations = 5 * len(relevantInbounds))
# run the optimizer outside the locking functions
self.releaseLock()
# perform the ACO run
aco = Colony(relevantInbounds, acoConfig)
aco.optimize()
self.acquireLock()
if None != aco.Result:
for node in aco.Result:
self.RecedingHorizonControl.resequenceInbound(node.Inbound)
# measure the exuction time of the overall optimization process
executionTime = time.process_time() - start
if 60.0 <= executionTime:
print('Optimized ' + str(len(aco.Result)) + ' inbounds in ' + str(executionTime) + ' seconds')
self.releaseLock()
def inboundSequence(self):
self.acquireLock()
sequence = self.RecedingHorizonControl.sequence()
self.releaseLock()
return sequence
def configure(self, configuration : AirportSequencing):
self.acquireLock()
self.SequencingConfiguration = configuration
self.releaseLock()

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

@@ -1,7 +1,9 @@
#!/usr/bin/env python
import argparse
from datetime import datetime
import os
import sys
from typing import Tuple
import zmq.auth
@@ -10,14 +12,12 @@ import zmq.auth
# @return The public and private key tuple
def KeyPairCreator(directory: str, server: bool) -> Tuple[str, str]:
if not server:
print('Creating a new pair for a client...')
target = 'client'
else:
print('Creating a new pair for the server...')
target = 'server'
public, private = zmq.auth.create_certificates(directory, target)
return (public, private)
return public, private
def str2bool(value):
if isinstance(value, bool):
@@ -29,16 +29,71 @@ def str2bool(value):
else:
raise argparse.ArgumentTypeError('Boolean value expected')
def findIdentificationKey(path, publicKey : bool):
if True == publicKey:
identifier = 'public-key = '
else:
identifier = 'secret-key = '
with open(path) as file:
key = ''
for line in file:
if identifier in line:
elements = line.split('=')
for idx in range(1, len(elements)):
if 0 == len(key):
key = elements[idx][2:-1]
key = key + elements[idx][-1]
else:
key = key + '=' + elements[idx]
return key[0:-2]
return None
if __name__ == '__main__':
# create the commandline parser
parser = argparse.ArgumentParser(description='Create a new key-value pair')
parser.add_argument('directory', help='Directory where to store the key pair')
parser.add_argument('--directory', type=str, help='Directory where to store the key pair')
parser.add_argument('--publickey', nargs='?', type=str, default=os.getcwd(), help='Full path to the public key of the server')
parser.add_argument('--server', default=False, action='store_true', help="Creates server key pair")
args = parser.parse_args()
# validate the arguments
if False == args.server and not os.path.exists(args.publickey):
sys.stderr.write('The public key of the server cannot be found')
sys.exit(-1)
# create the directory if it does not exist
if not os.path.exists(args.directory):
os.makedirs(args.directory)
# create the keys
KeyPairCreator(args.directory, args.server)
_, private = KeyPairCreator(args.directory, args.server)
if False == args.server:
publicServer = findIdentificationKey(args.publickey, True)
publicClient = findIdentificationKey(private, True)
privateClient = findIdentificationKey(private, False)
if None == publicServer:
sys.stderr.write('The public key of the server cannot be found in the defined file')
sys.exit(-1)
if None == publicClient:
sys.stderr.write('Unable to extract the created public key')
sys.exit(-1)
if None == privateClient:
sys.stderr.write('Unable to extract the created private key')
sys.exit(-1)
# rename keys
timestamp = str(datetime.now(tz=None))
timestamp = timestamp.replace(' ', '_')
timestamp = timestamp.replace(':', '-')
os.rename(os.path.join(args.directory, 'client.key'), os.path.join(args.directory, timestamp + '.key'))
os.rename(os.path.join(args.directory, 'client.key_secret'), os.path.join(args.directory, timestamp + '.key_secret'))
print(publicServer)
print(publicClient)
print(privateClient)

View File

@@ -118,13 +118,9 @@ if __name__ == '__main__':
print('Found ' + str(len(links)) + ' aircrafts')
aircrafts = []
parsed = 0
for link in links:
valid, aircraft = parsePerformanceData(link)
parsed += 1
print('Parsed ' + str(parsed) + ' of ' + str(len(links)), end='\r')
if False == valid:
print('Unable to find performance data for ' + link)
continue

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

@@ -2,10 +2,10 @@
class ArrivalRoute:
def __init__(self, name : str, runway : str, waypoints : list):
self.name = name
self.runway = runway
self.iaf = waypoints[0]
self.route = waypoints
self.Name = name
self.Runway = runway
self.Iaf = waypoints[0]
self.Route = waypoints
def __str__(self):
return 'Name: ' + self.name + ', IAF: ' + self.iaf.name + ', RWY: ' + self.runway
return 'Name: ' + self.Name + ', IAF: ' + self.Iaf.Name + ', RWY: ' + self.Runway

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)

View File

@@ -1,7 +1,45 @@
#!/usr/bin/env python
import pytz
from datetime import datetime
from aman.com import AircraftReport_pb2
from aman.sys.WeatherModel import WeatherModel
from aman.types.PerformanceData import PerformanceData
class Inbound:
def __init__(self, report : AircraftReport_pb2.AircraftReport):
self.report = report
def __init__(self, report : AircraftReport_pb2.AircraftReport, performanceData : PerformanceData):
self.Report = report
self.Callsign = report.aircraft.callsign
self.CurrentPosition = report.position
self.ReportTime = datetime.strptime(report.reportTime + '+0000', '%Y%m%d%H%M%S%z').replace(tzinfo = pytz.UTC)
self.EnrouteArrivalTime = None
self.InitialArrivalTime = None
self.RequestedRunway = None
self.MaximumTimeToGain = None
self.PlannedArrivalTime = None
self.PlannedRunway = None
self.PlannedStar = None
self.PlannedArrivalRoute = None
self.PlannedTrackmiles = None
self.FixedSequence = False
self.ExpectedRunway = None
self.AssignmentMode = None
self.HasValidSequence = False
self.WTC = None
# analyze the WTC
wtc = report.aircraft.wtc.upper()
if 'L' == wtc or 'M' == wtc or 'H' == wtc or 'J' == wtc:
self.WTC = wtc
# analyze the requested runway
if '' != report.requestedRunway:
self.RequestedRunway = report.requestedRunway
# search performance data -> fallback to A320
if self.Report.aircraft.type in performanceData.Aircrafts:
self.PerformanceData = performanceData.Aircrafts[self.Report.aircraft.type]
if None == self.PerformanceData:
self.PerformanceData = performanceData.Aircrafts['A320']

View File

@@ -2,17 +2,29 @@
class PerformanceData:
def __init__(self, icao : str):
self.icao = icao
self.speedAboveFL240 = 0.0
self.speedAboveFL100 = 0.0
self.speedBelowFL100 = 0.0
self.speedApproach = 0.0
self.rodAboveFL240 = 0.0
self.rodAboveFL100 = 0.0
self.rodBelowFL100 = 2000.0
self.Icao = icao
self.SpeedAboveFL240 = 0.0
self.SpeedAboveFL100 = 0.0
self.SpeedBelowFL100 = 0.0
self.SpeedApproach = 0.0
self.RodAboveFL240 = 0.0
self.RodAboveFL100 = 0.0
self.RodBelowFL100 = 2000.0
def ias(self, altitude, distance):
if 24000 < altitude:
return self.SpeedAboveFL240
elif 10000 < altitude:
return self.SpeedAboveFL100
elif 10000 >= altitude and 5 < distance:
return self.SpeedBelowFL100
elif 5 >= distance:
return self.SpeedApproach
else:
return self.SpeedBelowFL100
def __str__(self):
return 'ICAO: ' + self.icao + ', FL240: ' + str(self.rodAboveFL240) + '@' + str(self.speedAboveFL240) + \
', +FL100: ' + str(self.rodAboveFL100) + '@' + str(self.speedAboveFL100) + \
', -FL100: ' + str(self.rodBelowFL100) + '@' + str(self.speedBelowFL100) + \
', Vapp: ' + str(self.speedApproach)
return 'ICAO: ' + self.icao + ', FL240: ' + str(self.RodAboveFL240) + '@' + str(self.SpeedAboveFL240) + \
', +FL100: ' + str(self.RodAboveFL100) + '@' + str(self.SpeedAboveFL100) + \
', -FL100: ' + str(self.RodBelowFL100) + '@' + str(self.SpeedBelowFL100) + \
', Vapp: ' + str(self.SpeedApproach)

12
aman/types/Runway.py Normal file
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)

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
from sklearn.metrics.pairwise import haversine_distances
import numpy as np
import pyproj
class Waypoint:
def dms2dd(coordinate : str):
@@ -20,14 +20,59 @@ class Waypoint:
return dd
def __init__(self, name : str, latitude : float, longitude : float):
self.name = name
self.coordinate = np.array([ latitude, longitude ])
def coordinateArgument(value):
if True == isinstance(value, str):
return Waypoint.dms2dd(value)
elif True == isinstance(value, (float, int)):
return float(value)
else:
raise Exception('Invalid constructor argument')
def __init__(self, **kwargs):
self.Name = None
self.Coordinate = None
self.Altitude = None
self.Speed = None
self.BaseTurn = False
self.FinalTurn = False
latitude = None
longitude = None
for key, value in kwargs.items():
if 'name' == key.lower():
self.Name = str(value)
elif 'latitude' == key.lower():
latitude = Waypoint.coordinateArgument(value)
elif 'longitude' == key.lower():
longitude = Waypoint.coordinateArgument(value)
elif 'altitude' == key.lower():
self.Altitude = int(value)
elif 'speed' == key.lower():
self.Speed = int(value)
elif 'base' == key:
self.BaseTurn = bool(value)
elif 'final' == key:
self.FinalTurn = bool(value)
else:
raise Exception('Invalid constructor argument: ' + key)
if None != latitude and None != longitude:
self.Coordinate = np.array([ latitude, longitude ])
def __str__(self):
return 'Name: ' + self.name + ', Lat: ' + str(self.coordinate[0]) + ', Lon: ' + str(self.coordinate[1])
return 'Name: ' + self.Name + ', Lat: ' + str(self.Coordinate[0]) + ', Lon: ' + str(self.Coordinate[1])
def haversine(self, other):
self_radians = [np.radians(_) for _ in self.coordinate]
other_radians = [np.radians(_) for _ in other.coordinate]
return 6371.0 * haversine_distances([self_radians, other_radians])[0][1]
geodesic = pyproj.Geod(ellps='WGS84')
_, _, distance = geodesic.inv(self.Coordinate[1], self.Coordinate[0], other.Coordinate[1], other.Coordinate[0])
return distance / 1000.0 * 0.539957
def bearing(self, other):
geodesic = pyproj.Geod(ellps='WGS84')
forward, _, _ = geodesic.inv(self.Coordinate[1], self.Coordinate[0], other.Coordinate[1], other.Coordinate[0])
return forward
def project(self, bearing, distance):
geodesic = pyproj.Geod(ellps='WGS84')
longitude, latitude, _ = geodesic.fwd(self.Coordinate[1], self.Coordinate[0], bearing, distance)
return np.array([ latitude, longitude ])

View File

@@ -1,22 +0,0 @@
# Recat departure separation in seconds
# x = CAT A -> CAT F
# y = CAT A -> CAT F
# https://www.skybrary.aero/index.php/RECAT_-_Wake_Turbulence_Re-categorisation
recatDeparture = [
[0, 100, 120, 140, 160, 180],
[0, 0, 0, 100, 120, 140],
[0, 0, 0, 80, 100, 120],
[0, 0, 0, 0, 0, 120],
[0, 0, 0, 0, 0, 100],
[0, 0, 0, 0, 0, 80],
]
#Recat Arrival in NM
recatArrival = [
[3, 4, 5, 5, 6, 8],
[0, 3, 4, 4, 5, 7],
[0, 0, 3, 3, 4, 6],
[0, 0, 0, 0, 0, 5],
[0, 0, 0, 0, 0, 4],
[0, 0, 0, 0, 0, 3],
]

10
main.py
View File

@@ -1,10 +0,0 @@
from tcp.TCPServer import TCPServer
def main():
server = TCPServer()
server.run()
if __name__ == "__main__":
main()

View File

@@ -63,6 +63,7 @@ class build_py(_build_py):
generateProtobuf('src/protobuf/AircraftReport.proto')
generateProtobuf('src/protobuf/AircraftSchedule.proto')
generateProtobuf('src/protobuf/BaseTypes.proto')
generateProtobuf('src/protobuf/Communication.proto')
_build_py.run(self)
with open('README.md', 'r') as f:
@@ -77,13 +78,14 @@ setup(
'aman.config',
'aman.formats',
'aman.sys',
'aman.sys.aco',
'aman.tools',
'aman.types'
],
namespace_packages = [ 'aman' ],
description = 'AMAN optimization backend',
long_description = longDescription,
author = 'Sven Czarnian',
author = 'Sven Czarnian, Pascal Seeler',
author_email = 'devel@svcz.de',
license = 'GPLv3',
cmdclass = { 'clean': clean, 'build_py': build_py },
@@ -94,8 +96,9 @@ setup(
'numpy',
'protobuf',
'pyzmq',
'scikit-learn',
'scipy',
'setuptools'
'setuptools',
'flask',
'flask-cors'
]
)