Spaces:
Sleeping
Sleeping
Kr0issant commited on
Commit ·
a799e55
1
Parent(s): 53c0e4c
add observation state
Browse files- airports/VOCB.json +8 -1
- package-lock.json +6 -0
- rl_env/atc_gym.py +102 -4
- run_server.py +5 -3
- visualizer/package-lock.json +4 -4
airports/VOCB.json
CHANGED
|
@@ -197,7 +197,14 @@
|
|
| 197 |
"IAF_RWY_1",
|
| 198 |
"FAF_RWY_1"
|
| 199 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
}
|
| 201 |
},
|
| 202 |
-
"time_scale":
|
| 203 |
}
|
|
|
|
| 197 |
"IAF_RWY_1",
|
| 198 |
"FAF_RWY_1"
|
| 199 |
]
|
| 200 |
+
},
|
| 201 |
+
"N": {
|
| 202 |
+
"RWY_1_REV": [
|
| 203 |
+
"c3a2fccd-7b0f-45ab-8e0c-f9bcf1031cee",
|
| 204 |
+
"IAF_RWY_1_REV",
|
| 205 |
+
"FAF_RWY_1_REV"
|
| 206 |
+
]
|
| 207 |
}
|
| 208 |
},
|
| 209 |
+
"time_scale": 8.0
|
| 210 |
}
|
package-lock.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "atc_rl_api",
|
| 3 |
+
"lockfileVersion": 3,
|
| 4 |
+
"requires": true,
|
| 5 |
+
"packages": {}
|
| 6 |
+
}
|
rl_env/atc_gym.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
| 3 |
from openenv_core import Environment, Space
|
| 4 |
import numpy as np
|
| 5 |
from core.engine import SimulationEngine
|
|
|
|
| 6 |
|
| 7 |
class ATCEnv(Environment):
|
| 8 |
"""
|
|
@@ -43,10 +44,107 @@ class ATCEnv(Environment):
|
|
| 43 |
return obs, reward, done, truncated, {}
|
| 44 |
|
| 45 |
def get_observation(self):
|
| 46 |
-
"""Get
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
def compute_reward(self):
|
| 52 |
"""Reward function: +1 for distance to runway, -100 for collision"""
|
|
|
|
| 3 |
from openenv_core import Environment, Space
|
| 4 |
import numpy as np
|
| 5 |
from core.engine import SimulationEngine
|
| 6 |
+
import math
|
| 7 |
|
| 8 |
class ATCEnv(Environment):
|
| 9 |
"""
|
|
|
|
| 44 |
return obs, reward, done, truncated, {}
|
| 45 |
|
| 46 |
def get_observation(self):
|
| 47 |
+
"""Get structured observation for LLM/Agent consumption"""
|
| 48 |
+
|
| 49 |
+
segments_labels = ["North", "North-East", "East", "South-East",
|
| 50 |
+
"South", "South-West", "West", "North-West"]
|
| 51 |
+
|
| 52 |
+
obs = {
|
| 53 |
+
"aircraft": [],
|
| 54 |
+
"airport_status": {
|
| 55 |
+
"active_runways": list(self.engine.active_runways),
|
| 56 |
+
"runway_occupancy": {},
|
| 57 |
+
"wind": {
|
| 58 |
+
"heading": int(self.engine.wind_heading),
|
| 59 |
+
"speed": int(self.engine.wind_speed)
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
for rw, status in self.engine.runway_status.items():
|
| 65 |
+
obs["airport_status"]["runway_occupancy"][rw] = status.get("occupied_by")
|
| 66 |
+
|
| 67 |
+
aircraft_list = list(self.engine.aircrafts.values())
|
| 68 |
+
|
| 69 |
+
for i, ac in enumerate(aircraft_list):
|
| 70 |
+
# Compute position
|
| 71 |
+
distance = round(math.sqrt(ac.x**2 + ac.y**2), 2)
|
| 72 |
+
if ac.x == 0 and ac.y == 0:
|
| 73 |
+
bearing = 0.0
|
| 74 |
+
else:
|
| 75 |
+
bearing = (90 - math.degrees(math.atan2(ac.y, ac.x))) % 360
|
| 76 |
+
segment_idx = round(bearing / 45) % 8
|
| 77 |
+
segment_name = segments_labels[segment_idx]
|
| 78 |
+
|
| 79 |
+
# Compute separation
|
| 80 |
+
closest_callsign = None
|
| 81 |
+
closest_dist = float('inf')
|
| 82 |
+
conflict_risk = "none"
|
| 83 |
+
|
| 84 |
+
for j, other_ac in enumerate(aircraft_list):
|
| 85 |
+
if i != j:
|
| 86 |
+
dist_km = math.sqrt((ac.x - other_ac.x)**2 + (ac.y - other_ac.y)**2)
|
| 87 |
+
if dist_km < closest_dist:
|
| 88 |
+
closest_dist = dist_km
|
| 89 |
+
closest_callsign = other_ac.callsign
|
| 90 |
+
alt_diff = abs(ac.altitude - other_ac.altitude)
|
| 91 |
+
if dist_km < 5.0 and alt_diff < 1500:
|
| 92 |
+
conflict_risk = "high"
|
| 93 |
+
elif dist_km < 10.0 and alt_diff < 3000 and conflict_risk != "high":
|
| 94 |
+
conflict_risk = "medium"
|
| 95 |
+
|
| 96 |
+
if closest_dist == float('inf'):
|
| 97 |
+
closest_dist_val = None
|
| 98 |
+
else:
|
| 99 |
+
closest_dist_val = round(closest_dist, 2)
|
| 100 |
+
|
| 101 |
+
# Compute alerts
|
| 102 |
+
alerts = []
|
| 103 |
+
if ac.fuel_level < 10 or ac.emergency_index >= 1:
|
| 104 |
+
alerts.append("low_fuel")
|
| 105 |
+
if ac.emergency_index == 3:
|
| 106 |
+
alerts.append("critical_emergency")
|
| 107 |
+
|
| 108 |
+
# Intent block additions
|
| 109 |
+
dist_to_thresh = None
|
| 110 |
+
if ac.runway_threshold:
|
| 111 |
+
tx, ty = ac.runway_threshold["x"], ac.runway_threshold["y"]
|
| 112 |
+
dist_to_thresh = round(math.sqrt((tx - ac.x)**2 + (ty - ac.y)**2), 2)
|
| 113 |
+
|
| 114 |
+
next_wp = ac.active_star if ac.active_star else "None"
|
| 115 |
+
if hasattr(ac, "direct_to_wp") and ac.direct_to_wp:
|
| 116 |
+
next_wp = ac.direct_to_wp.get("name", "Direct")
|
| 117 |
+
|
| 118 |
+
ac_obj = {
|
| 119 |
+
"callsign": ac.callsign,
|
| 120 |
+
"position": {
|
| 121 |
+
"segment": segment_name,
|
| 122 |
+
"distance": distance,
|
| 123 |
+
"altitude": int(ac.altitude),
|
| 124 |
+
"target_altitude": int(ac.target_alt)
|
| 125 |
+
},
|
| 126 |
+
"motion": {
|
| 127 |
+
"heading": round(ac.heading, 1),
|
| 128 |
+
"target_heading": round(ac.target_heading, 1),
|
| 129 |
+
"speed": int(ac.speed),
|
| 130 |
+
"target_speed": int(ac.target_speed)
|
| 131 |
+
},
|
| 132 |
+
"intent": {
|
| 133 |
+
"state": ac.state,
|
| 134 |
+
"assigned_runway": ac.target_runway_id,
|
| 135 |
+
"distance_to_threshold": dist_to_thresh,
|
| 136 |
+
"next_waypoint": next_wp
|
| 137 |
+
},
|
| 138 |
+
"alerts": alerts,
|
| 139 |
+
"separation": {
|
| 140 |
+
"closest_traffic": closest_callsign,
|
| 141 |
+
"distance": closest_dist_val,
|
| 142 |
+
"conflict_risk": conflict_risk
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
obs["aircraft"].append(ac_obj)
|
| 146 |
+
|
| 147 |
+
return obs
|
| 148 |
|
| 149 |
def compute_reward(self):
|
| 150 |
"""Reward function: +1 for distance to runway, -100 for collision"""
|
run_server.py
CHANGED
|
@@ -32,11 +32,13 @@ def run_server():
|
|
| 32 |
parent_dir = os.path.dirname(script_dir)
|
| 33 |
ui_dir = os.path.join(script_dir, "visualizer")
|
| 34 |
|
|
|
|
|
|
|
|
|
|
| 35 |
if args.only_ui:
|
| 36 |
print("Starting ONLY Frontend (Vite)...")
|
| 37 |
try:
|
| 38 |
-
|
| 39 |
-
subprocess.run(["npm", "run", "dev"], cwd=ui_dir, shell=True)
|
| 40 |
except KeyboardInterrupt:
|
| 41 |
print("\nStopping UI...")
|
| 42 |
return
|
|
@@ -48,7 +50,7 @@ def run_server():
|
|
| 48 |
if args.ui:
|
| 49 |
print("Starting Frontend (Vite)...")
|
| 50 |
try:
|
| 51 |
-
ui_process = subprocess.Popen([
|
| 52 |
print(f"Frontend process started (PID: {ui_process.pid})")
|
| 53 |
except Exception as e:
|
| 54 |
print(f"Error starting frontend: {e}")
|
|
|
|
| 32 |
parent_dir = os.path.dirname(script_dir)
|
| 33 |
ui_dir = os.path.join(script_dir, "visualizer")
|
| 34 |
|
| 35 |
+
# Helper to run npm commands robustly
|
| 36 |
+
npm_exe = "npm.cmd" if os.name == 'nt' else "npm"
|
| 37 |
+
|
| 38 |
if args.only_ui:
|
| 39 |
print("Starting ONLY Frontend (Vite)...")
|
| 40 |
try:
|
| 41 |
+
subprocess.run([npm_exe, "run", "dev"], cwd=ui_dir)
|
|
|
|
| 42 |
except KeyboardInterrupt:
|
| 43 |
print("\nStopping UI...")
|
| 44 |
return
|
|
|
|
| 50 |
if args.ui:
|
| 51 |
print("Starting Frontend (Vite)...")
|
| 52 |
try:
|
| 53 |
+
ui_process = subprocess.Popen([npm_exe, "run", "dev"], cwd=ui_dir)
|
| 54 |
print(f"Frontend process started (PID: {ui_process.pid})")
|
| 55 |
except Exception as e:
|
| 56 |
print(f"Error starting frontend: {e}")
|
visualizer/package-lock.json
CHANGED
|
@@ -2497,9 +2497,9 @@
|
|
| 2497 |
}
|
| 2498 |
},
|
| 2499 |
"node_modules/vite": {
|
| 2500 |
-
"version": "8.0.
|
| 2501 |
-
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.
|
| 2502 |
-
"integrity": "sha512-
|
| 2503 |
"dev": true,
|
| 2504 |
"license": "MIT",
|
| 2505 |
"dependencies": {
|
|
@@ -2524,7 +2524,7 @@
|
|
| 2524 |
"peerDependencies": {
|
| 2525 |
"@types/node": "^20.19.0 || >=22.12.0",
|
| 2526 |
"@vitejs/devtools": "^0.1.0",
|
| 2527 |
-
"esbuild": "^0.27.0",
|
| 2528 |
"jiti": ">=1.21.0",
|
| 2529 |
"less": "^4.0.0",
|
| 2530 |
"sass": "^1.70.0",
|
|
|
|
| 2497 |
}
|
| 2498 |
},
|
| 2499 |
"node_modules/vite": {
|
| 2500 |
+
"version": "8.0.5",
|
| 2501 |
+
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.5.tgz",
|
| 2502 |
+
"integrity": "sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ==",
|
| 2503 |
"dev": true,
|
| 2504 |
"license": "MIT",
|
| 2505 |
"dependencies": {
|
|
|
|
| 2524 |
"peerDependencies": {
|
| 2525 |
"@types/node": "^20.19.0 || >=22.12.0",
|
| 2526 |
"@vitejs/devtools": "^0.1.0",
|
| 2527 |
+
"esbuild": "^0.27.0 || ^0.28.0",
|
| 2528 |
"jiti": ">=1.21.0",
|
| 2529 |
"less": "^4.0.0",
|
| 2530 |
"sass": "^1.70.0",
|