Runway Controller: Priorities, Spacing, Wind and Scale
HardAI-assisted coding · AI-Assisted Feature Work
Runway Controller: Priorities, Spacing, Wind and Scale
You've joined the team that builds the runway scheduling core for a regional airport. Flights ask for a runway slot; the controller decides who goes next, on which runway, and when. The first release handles one runway with no spacing rules, and controllers have already complained about who it clears first. Spacing, multiple runways with wind closures, and a faster engine for the daily timetable are queued behind that.
Codebase
| File | What it does |
|---|---|
runway/api.py | run(operations) entry point and the Dispatcher that routes each operation |
runway/scheduler.py | Scheduler: validation, requests, clearing flights, drain |
runway/queue.py | FlightQueue: the waiting line and its ordering |
runway/runways.py | Runway (per-runway state) and RunwayPool (choosing a runway) |
runway/separation.py | SeparationTable: minimum gaps between operations (stub) |
runway/wind.py | runway_closed: wind closure rule (stub) |
runway/models.py | FlightRequest, Clearance and Wind dataclasses |
runway/config.py, runway/errors.py | Constants and error codes |
tests/ | Existing unit and end-to-end tests (some currently fail) |
How your code is run
run(operations: list[list]) -> list executes the operations in order against a fresh controller and returns one result per operation. Every operation is [name, ts, ...args]; ts is an integer number of seconds and never decreases. Errors are returned as strings, never raised.
| Operation | Arguments after ts | Returns | Errors (checked in this order) |
|---|---|---|---|
request | flight_id, kind | true | invalid arguments, invalid kind, flight exists |
request_batch | prefix, count, kinds | count | invalid arguments, invalid kind, flight exists |
cancel | flight_id | true | flight not found |
queue | optional limit | flight ids in the order they would be cleared | invalid arguments |
clear_next | none | [flight_id, runway_id, start], or null if nobody is waiting | no open runway |
drain | none | summary object (see below) | |
set_separation *(Part 2)* | previous_kind, next_kind, seconds | true | invalid kind, invalid seconds |
add_runway *(Part 3)* | runway_id, heading | true | invalid arguments, invalid heading, runway exists |
set_wind *(Part 3)* | direction, speed | true | invalid wind |
runway_status *(Part 3)* | none | [[runway_id, "open" or "closed"], ...] sorted by id |
- Each error is returned as
"error: <code>". An unknown operation name returns"error: unknown operation"; a wrong number of arguments, a non-integerts, an empty or non-string id, or a non-string kind returns"error: invalid arguments". - A kind is
"emergency"(an emergency landing),"landing"or"takeoff". - A flight id can be requested only once, ever: it stays taken after the flight is cleared or cancelled (
flight exists).cancelonly works on a waiting flight; a cleared, cancelled or unknown flight returnsflight not found. request_batchimportscountflights (1to100000) named<prefix>1...<prefix>N, all at the samets. Their kinds cycle through thekindslist (a non-empty list of kinds). It is all-or-nothing: if any generated id is already taken, nothing is added.queuelists waiting flights in clearing order; with alimit(an integer>= 0) only that many.clear_nextremoves the next flight from the line and assigns it to a runway. It returns[flight_id, runway_id, start], wherestartis when its operation begins (seconds). The runway you start with is"R1".- Clearing order. Emergencies are served before landings, and landings before takeoffs. Within a kind, flights are served first come first served, meaning in the order their
request(or position within arequest_batch) was received, not alphabetically and not byts. draincallsclear_nextrepeatedly, all at the drain's ownts, until nobody is waiting (or no runway can take the next flight, see Part 3). It returns{"cleared": n, "last_start": s, "checksum": c}: how many flights were cleared, thestartof the last one (nullif none), and a checksum that is the runningzlib.crc32over the ASCII text"<flight_id>:<runway_id>:<start>"of each clearance in order (starting from 0). Two runs that clear the same flights in the same order always produce the same checksum.
Worked example
[["request", 0, "AA10", "takeoff"],
["request", 1, "UA22", "landing"],
["request", 2, "MED1", "emergency"],
["request", 3, "DL5", "takeoff"],
["queue", 4],
["clear_next", 5],
["clear_next", 6],
["cancel", 7, "AA10"],
["clear_next", 8],
["clear_next", 9]]returns
[true, true, true, true, ["MED1", "UA22", "AA10", "DL5"],
["MED1", "R1", 5], ["UA22", "R1", 6], true, ["DL5", "R1", 8], null]Part 1 — Fix the clearing order (ATC-871, ATC-880)
Goal: the controller clears flights in the documented order.
Reports:
- "An emergency landing sat in the line while routine takeoffs were cleared ahead of it."
- "Two takeoffs requested in the order
B7thenA3were cleared asA3first."
Rules: the clearing order in the section above is the contract: emergencies, then landings, then takeoffs, and first come first served within each kind. queue and clear_next must agree.
Part 2 — Minimum separation (ATC-902)
Goal: a runway can only be used again after the required gap.
set_separation configures a gap in seconds for a (previous kind, next kind) pair, e.g. ["set_separation", 0, "takeoff", "takeoff", 90]. Unset pairs have a gap of 0, so until separations are configured everything behaves exactly as in Part 1.
Rules
previous_kindandnext_kindmust be valid kinds (invalid kind, previous checked first);secondsmust be an integer>= 0(booleans are not integers) or the call returnsinvalid seconds. Setting a pair again replaces its value. The pair is directional:landing -> takeoffis configured separately fromtakeoff -> landing.- Each runway remembers the start time and kind of the last flight cleared on it. A new flight of kind
kcleared byclear_nextattsstarts atmax(ts, last_start + gap(last_kind, k)). The gap is measured from the previous flight's start to this flight's start, and a start exactly atlast_start + gapis allowed. A runway that has not been used yet imposes no delay. - Emergencies are never delayed: an emergency starts at
ts. It still counts as the runway's last flight afterwards, so the next flight is spaced from the emergency's start. - The clearing order (Part 1) does not change; only
startdoes.startmay be later thants.drainuses its owntsfor every flight, so the starts it produces are spaced out.
Example
[["set_separation", 0, "takeoff", "takeoff", 90],
["set_separation", 0, "landing", "takeoff", 60],
["set_separation", 0, "takeoff", "emergency", 120],
["request", 1, "T1", "takeoff"],
["request", 1, "T2", "takeoff"],
["request", 1, "L1", "landing"],
["clear_next", 10],
["clear_next", 20],
["clear_next", 30],
["request", 161, "E1", "emergency"],
["clear_next", 162]]returns [true, true, true, true, true, true, ["L1","R1",10], ["T1","R1",70], ["T2","R1",160], true, ["E1","R1",162]]. L1 goes first (landing before takeoff). T1 follows a landing, so it waits until 10 + 60. T2 follows a takeoff, so it waits until 70 + 90. E1 ignores the 120-second gap.
Part 3 — Multiple runways and wind closures (ATC-934)
Goal: the airport can have several runways, and strong wind closes some of them.
runway_status, add_runway and set_wind are the new operations. The airport still starts with only "R1" (heading 90) and calm wind, so Parts 1 and 2 are unchanged until you use them.
Rules
add_runwaytakes a non-empty string id and aheadingthat is an integer from0to359(otherwiseinvalid heading); an id that already exists (including"R1") returnsrunway exists.set_windtakesdirection(the integer degrees the wind blows from,0to359) andspeed(integer knots>= 0), otherwiseinvalid wind. It replaces the previous wind and stays in force until changed. The initial wind is speed0.- A runway is closed when
speed > 25, or whenspeed > 12and the wind is more than60degrees off the runway's axis. The runway can be used in either direction, so the off-axis angle isgap = abs(direction - heading) % 180, thenmin(gap, 180 - gap). It is a value from0to90; exactly60is not "more than 60". Heading350and wind10are 20 degrees apart, not 340. clear_nexttakes the next flight in clearing order and chooses among the open runways the one where it can start soonest (Part 2's rule applied per runway, each runway with its own last flight); ties go to the runway with the smaller id (plain string comparison). If the line is empty it returnsnullfirst; otherwise, if every runway is closed, it returnsno open runwayand the flight stays in line.drainstops, without error, as soon as no open runway is available; it reports what it cleared.runway_statuslists[id, "open" | "closed"]for each runway, sorted by id.
Example
[["add_runway", 0, "R2", 0],
["runway_status", 0],
["set_wind", 1, 0, 20],
["runway_status", 1],
["set_separation", 1, "takeoff", "takeoff", 60],
["request", 2, "A1", "takeoff"],
["request", 2, "A2", "takeoff"],
["clear_next", 3],
["clear_next", 4],
["set_wind", 5, 0, 30],
["request", 6, "A3", "takeoff"],
["clear_next", 7],
["set_wind", 8, 90, 5],
["clear_next", 9]]returns
[true, [["R1","open"],["R2","open"]], true, [["R1","closed"],["R2","open"]], true, true, true,
["A1","R2",3], ["A2","R2",63], true, true, "error: no open runway", true, ["A3","R1",9]]A wind from 0 at 20 knots is 90 degrees off R1's axis, so R1 is closed while R2 (heading 0) stays open. At 30 knots both close. Back in calm wind, R1 can start A3 at 9 while R2 would have to wait until 63 + 60.
Part 4 — The daily timetable (stretch)
Goal: request_batch followed by drain must stay fast with thousands of flights.
Airline schedules are imported in bulk. A hidden test imports thousands of flights (with a mix of kinds), cancels some of them, configures separations, runs several runways with wind changes, and drains the whole line. The output must be identical to the straightforward implementation (the same clearings in the same order, hence the same checksum), but each run has a time limit of a few seconds. queue with a small limit is also used on large lines.
Notes
models.py,config.pyandapi.pyare known-good.FlightQueueand the wayScheduleruses it is where the performance work belongs.- You may use the AI assistant freely. Later parts must not break earlier ones.
- Parts 1-3 are a strong result; Part 4 is a stretch.
Example 1
- Input:
- [["request",0,"AA10","takeoff"],["request",1,"UA22","landing"],["request",2,"MED1","emergency"],["request",3,"DL5","takeoff"],["queue",4],["clear_next",5],["clear_next",6],["cancel",7,"AA10"],["clear_next",8],["clear_next",9]]
- Output:
- [true,true,true,true,["MED1","UA22","AA10","DL5"],["MED1","R1",5],["UA22","R1",6],true,["DL5","R1",8],null]
- Explanation
- Emergency first, then the landing, then takeoffs in request order. AA10 is cancelled before it is cleared, so DL5 goes next and the following clear_next finds nobody waiting.
Example 2
- Input:
- [["set_separation",0,"takeoff","takeoff",90],["set_separation",0,"landing","takeoff",60],["set_separation",0,"takeoff","emergency",120],["request",1,"T1","takeoff"],["request",1,"T2","takeoff"],["request",1,"L1","landing"],["clear_next",10],["clear_next",20],["clear_next",30],["request",161,"E1","emergency"],["clear_next",162]]
- Output:
- [true,true,true,true,true,true,["L1","R1",10],["T1","R1",70],["T2","R1",160],true,["E1","R1",162]]
- Explanation
- Starts are spaced from the previous flight's start: T1 waits 60s after the landing, T2 waits 90s after T1. The emergency ignores the 120s gap and starts at its own ts.
Example 3
- Input:
- [["add_runway",0,"R2",0],["runway_status",0],["set_wind",1,0,20],["runway_status",1],["set_separation",1,"takeoff","takeoff",60],["request",2,"A1","takeoff"],["request",2,"A2","takeoff"],["clear_next",3],["clear_next",4],["set_wind",5,0,30],["request",6,"A3","takeoff"],["clear_next",7],["set_wind",8,90,5],["clear_next",9]]
- Output:
- [true,[["R1","open"],["R2","open"]],true,[["R1","closed"],["R2","open"]],true,true,true,["A1","R2",3],["A2","R2",63],true,true,"error: no open runway",true,["A3","R1",9]]
- Explanation
- A 20-knot wind from 0 degrees is 90 degrees off R1's axis, so only R2 is open and both takeoffs use it. At 30 knots everything closes and the flight stays in line. Back in calm wind R1 (never used) can start it at once.
Guidelines
- Python 3 standard library only
- Timestamps and start times are integer seconds; ts never decreases from one operation to the next
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised
- Clearing order is fully deterministic: kind urgency, then request order, then (for runways) earliest start, then smallest runway id
- Part 4 inputs import and drain thousands of flights; each run has a few seconds