Using the BluebirdATC API with a non-Python Agent¶
For developing Agents in Python, we recommend using the bluebird-gymnasium package within BluebirdATC. This gives access to a gymnasium environment allowing fast and easy training via wrappers to the simulation Environment.
However, it is also possible to receive the Environment and send Actions via a REST API, meaning that Agents can be written in any language (as long as they can make and receive HTTP requests).
In this notebook, we will create a simple agent in the Julia programming language. However, the details of the language are not important - the key thing to note is the HTTP requests themselves, which would be common to any other programming language.
Pre-requisites for running this notebook¶
Julia and necessary packages:
- Install Julia following the instructions on https://julialang.org/downloads
- Install the IJulia, HTTP, JSON and Plots packages from the Julia command line by pressing
]to enter package mode and typing:add IJulia add JSON add HTTP add Plots
Run the bluebird-dt API
- In another terminal, start up the API server by navigating to the directory
BluebirdATC/bluebird-apiand typing the command:uv run uvicorn bluebird_api:app --port 8000
Load a simple scenario¶
We can look at the available scenarios, and choose the simplest one - two aircraft on the I-Sector.
using HTTP
r = HTTP.request("GET", "http://localhost:8000/list_scenario_categories")
HTTP.Messages.Response: """ HTTP/1.1 200 OK date: Thu, 02 Apr 2026 15:45:32 GMT server: uvicorn content-length: 39 content-type: application/json ["Artificial","Springfield","Infinite"]"""
r = HTTP.request("GET", "http://localhost:8000/list_scenarios/Artificial")
HTTP.Messages.Response: """ HTTP/1.1 200 OK date: Thu, 02 Apr 2026 15:45:36 GMT server: uvicorn content-length: 73 content-type: application/json ["I-Sector Two Aircraft","X-Sector Two Aircraft","Y-Sector Two Aircraft"]"""
r = HTTP.request("POST", "http://localhost:8000/load/Artificial/I-Sector%20Two%20Aircraft")
HTTP.Messages.Response: """ HTTP/1.1 200 OK date: Thu, 02 Apr 2026 15:45:43 GMT server: uvicorn content-length: 4 content-type: application/json true"""
Getting the Environment¶
We can access a JSON structure representing the full simulation environment via the following HTTP request:
r = HTTP.request("GET", "http://localhost:8000/environment")
HTTP.Messages.Response:
"""
HTTP/1.1 200 OK
date: Thu, 02 Apr 2026 15:45:53 GMT
server: uvicorn
content-length: 1395
content-type: application/json
vary: Accept-Encoding
content-encoding: gzip
{"time":6.0,"start_time":0,"aircraft":{"AIR0":{"lat":50.073112723077266,"lon":-3.5331101696407354,"fl":300.0,"heading":359.9773006147675,"flight_plan":{"route":{"filed":["FIRE","EARTH","WATER","AIR","SPIRIT"],"current":["FIRE","EARTH","WATER","AIR","SPIRIT"]},"unexpanded_route":"EARTH WATER AIR","origin":null,"dest":null,"milcivil":null,"sector_crossing_seq":"","requested_flight_level":null,"filed_true_airspeed":null,"intention_code":null,"assigned_squawk":null,"start_datetime":null,"end_datetime":null},"callsign":"AIR0","ufid":null,"rate_of_turn":null,"aircraft_type":"B753","operation_params":{},"controllable":true,"simulated":true,"current_sector":"sector_i","previous_sector":"background","percentile_rank_dict":{"cas_des":null,"cas_cr":null,"cas_cl":null,"rocd_des":null,"rocd_cl":null},"pilot":{"pilot_type":"Pilot","callsign":"AIR0"},"squawk":null,"squawk_ident_until":null,"wake_vortex":"M","speed_tas":234.27870386437758,"vertical_speed":0.0,"ground_speed":234.27870386437758,"ground_
⋮
5886-byte body
"""
using JSON
env = JSON.parse(String(r.body))
JSON.Object{String, Any} with 7 entries:
"time" => 6.0
"start_time" => 0
"aircraft" => Object{String, Any}("AIR0"=>Object{String, Any}("lat"=>50.…
"coordinations" => Any[Object{String, Any}("callsign"=>"AIR0", "from_sector"=…
"wind_field" => nothing
"forecast" => nothing
"airspace" => Object{String, Any}("sectors"=>Object{String, Any}("sector…
Sending an Action¶
We can use the following endpoint and data structure to send an Action to one of the aircraft in the environment (let's pick the one with callsign "AIR0"):
# Define an action in a dictionary
action = Dict("agent" => "dummy", "callsign" => "AIR0", "kind" => "change_heading_to", "value" => 90, "sector" => "sector_i")
# the endpoint expects a list of actions
actions = [action]
# send the request with the JSON-encoded action as the
r = HTTP.post("http://localhost:8000/actions", body=JSON.json(actions))
HTTP.Messages.Response: """ HTTP/1.1 200 OK date: Thu, 02 Apr 2026 15:46:03 GMT server: uvicorn content-length: 4 content-type: application/json true"""
Creating a "turn left" Agent.¶
One simple/naive thing we could do to avoid conflicts is to tell all aircraft to turn left by 90 degrees if there is another aircraft within 0.1 degrees latitude and longitude.
We can implement this as a function that takes in the environment and returns a list of actions.
function turn_left_agent(environment)
actions = []
# double loop over all aircraft in environment, to find all 2-aircraft combinations
vals = collect(values(environment["aircraft"]))
for i in 1:length(vals)
for j in (i+1):length(vals)
aircraft_1 = vals[i]
aircraft_2 = vals[j]
if (abs(aircraft_1["lat"] - aircraft_2["lat"]) < 0.1) && (abs(aircraft_1["lon"] - aircraft_2["lon"]) < 0.1)
# tell both aircraft to change heading by -90 degrees (i.e. turn left)
action_1 = Dict("agent" => "turn_left", "callsign" => aircraft_1["callsign"], "kind" => "change_heading_by", "value" => -90, "sector" => "sector_i")
action_2 = Dict("agent" => "turn_left", "callsign" => aircraft_2["callsign"], "kind" => "change_heading_by", "value" => -90, "sector" => "sector_i")
push!(actions, action_1)
push!(actions, action_2)
end
end
end
return actions
end
turn_left_agent (generic function with 1 method)
Run the Agent, and visualise the result¶
We can now step through the simulation, using the "evolve/{step_size}" endpoint, and on each step we retrieve the environment, pass it to the turn_left_agent, and send any resulting Actions to the "actions" endpoint.
We will use the "Plots" library to create a video of the resulting behaviour. We first need to create some utility functions:
using Plots
function parse_coords(s::String)
m = match(r"([\d.]+)([NS])\s+([\d.]+)([EW])", s)
lat = parse(Float64, m[1]) * (m[2] == "S" ? -1 : 1)
lon = parse(Float64, m[3]) * (m[4] == "W" ? -1 : 1)
return lon, lat # (x, y) order for plotting
end
parse_coords (generic function with 1 method)
function draw_sector(environment)
coords = parse_coords.(env["airspace"]["sectors"]["sector_i"]["volumes"][1]["area"])
# Close the polygon by repeating the first point
push!(coords, coords[1])
lons = first.(coords)
lats = last.(coords)
plot(
lons, lats,
seriestype = :shape,
fillalpha = 0.35,
fillcolor = :steelblue,
linecolor = :darkblue,
linewidth = 2,
legend = false,
xlabel = "Longitude",
ylabel = "Latitude",
title = "I-Sector",
aspect_ratio = :equal
)
end
draw_sector (generic function with 1 method)
function draw_aircraft!(aircraft; color=:red, size=0.05, npoints=6, inner_ratio=0.05)
cx = aircraft["lon"]
cy = aircraft["lat"]
# Build the star polygon by alternating outer and inner vertices
n_verts = 2 * npoints
angles = [(i * π / npoints) - π/2 for i in 0:(n_verts - 1)]
radii = [isodd(i) ? size * inner_ratio : size for i in 0:(n_verts - 1)]
xs = cx .+ radii .* cos.(angles)
ys = cy .+ radii .* sin.(angles)
# Close the polygon
push!(xs, xs[1])
push!(ys, ys[1])
plot!(xs, ys,
seriestype = :shape,
fillcolor = color,
fillalpha = 1.0,
linecolor = :darkorange,
linewidth = 1,
label = false
)
annotate!(cx+0.18, cy, text(aircraft["callsign"]))
end
draw_aircraft! (generic function with 1 method)
function draw_all_aircraft(environment)
draw_sector(environment)
for val in values(environment["aircraft"])
# draw the aircraft
draw_aircraft!(val)
end
end
draw_all_aircraft (generic function with 1 method)
Putting it all together¶
anim = @animate for step in 1:200
r = HTTP.post("http://localhost:8000/evolve/6")
r = HTTP.get("http://localhost:8000/environment")
env = JSON.parse(String(r.body))
if step == 1
draw_sector(step)
end
draw_all_aircraft(env)
actions = turn_left_agent(env)
r = HTTP.post("http://localhost:8000/actions", body=JSON.json(actions))
end
Animation("/tmp/jl_dmiued", ["000001.png", "000002.png", "000003.png", "000004.png", "000005.png", "000006.png", "000007.png", "000008.png", "000009.png", "000010.png" … "000191.png", "000192.png", "000193.png", "000194.png", "000195.png", "000196.png", "000197.png", "000198.png", "000199.png", "000200.png"])
Conclusions¶
We have demonstrated a trivial agent that probably isn't much use for getting aircraft to where they need to get to, but it does illustrate the workflow of:
- Loading a simulated scenario, using the
/load/{category}/{scenario_name}endpoint - Stepping through the simulation in increments of
step_sizeusing the/evolve/{step_size}endpoint - Getting the environment at each point using the
/environmentendpoint - Passing that environment to our Agent and getting a list of Actions back.
- Passing those Actions to the simulation using the
/actionsendpoint.