Conformance monitor¶
Available now: ground, airborne, landing, pattern and airspace rules, all wired into the engine and speaking.
Five monitors watch whether the aircraft is actually doing what it's been cleared to do,
and escalate a callout the way a real controller would when it isn't: one for ground
movement, one for everything from the takeoff roll through approach, one for the landing
itself, one for VFR pattern work, and one for VFR Class B/C/D
airspace entry. All five are pure (no I/O, deterministic on
AircraftState.t), and all five are checked every tick from AtcEngine.on_tick.
Ground conformance¶
xatc.atc.conformance.ConformanceMonitor -- src/xatc/atc/conformance.py.
The four rules¶
| Rule | Fires when | Escalation |
|---|---|---|
| Taxiing without clearance | Moving on the ground before taxi_cleared is set, sustained past a short threshold |
Gentle → firm → "possible pilot deviation" |
| Off the taxi route | Cleared to taxi, but farther from the assigned route than tolerance allows, sustained | Gentle → firm → "possible pilot deviation" |
| Runway incursion | Inside a runway's protected zone without a clearance that allows it -- fires immediately, no sustain time | "Hold position!" straight to "possible pilot deviation" |
| Takeoff without clearance | On runway pavement, aligned with it, moving, without a takeoff clearance | "Stop immediately" straight to "possible pilot deviation" |
Runway ids are canonicalized everywhere the runway-incursion rule looks them up, fixing
a real bug where an apt.dat file that spells the same runway inconsistently between
its own row and its hold-short rows (e.g. KLAX's "6R" vs. "06R") could falsely flag a
legitimate, correctly-flown runway crossing as an incursion, or name the wrong runway
end in the incursion event's own text.
Every rule fires once when a violation is established, steps up exactly one severity level for each interval the violation continues (never repeating a step), and resets once the aircraft has been back in conformance for a short recovery window -- so a brief dip doesn't restart the ladder. A stopped aircraft (under 1 kt) doesn't keep escalating an off-route call, since it's holding position as told. The off-route rule is suppressed inside a runway's protected zone and whenever the clearance covers a line-up-and-wait or takeoff, so an incursion never doubles up with an off-route call for the same moment.
The "taxiing without clearance" rule also catches an unapproved pushback: moving more than 50 m from where the aircraft was parked, while still in a parked phase and without a pushback or taxi clearance, fires the same ladder -- pushback creep is exempt from the taxi speed check but not this distance check.
Verification¶
30 unit tests cover each rule's sustain boundary, the exact escalation timing, the reset
window, strictness scaling, and runway-crossing edge cases. A real recorded KSEA taxi-out
flight, replayed with a matching clearance, produces zero false-positive events at
every strictness level -- and the same replay with no taxi clearance at all correctly
fires the taxi-without-clearance rule. That verification runs two ways: directly against
ConformanceMonitor, and end to end through the wired-up AtcEngine, which is what
actually ships.
Airborne conformance¶
xatc.atc.conformance_airborne.AirborneConformanceMonitor --
src/xatc/atc/conformance_airborne.py. Runs alongside the ground monitor, checking
altitude, heading, speed and squawk against the current clearance once the aircraft is
off the ground.
The four rules¶
| Rule | Fires when | Ladder |
|---|---|---|
| Altitude deviation | More than the tolerance off the assigned altitude once the aircraft has reached it (an overshoot counts -- passing through the target reaches it). Before that, only moving the wrong way from it, or missing a climb/descent allowance, fires. A normal climb or descent toward a new assignment never fires. | Gentle → firm → "possible pilot deviation" |
| Heading deviation | More than the tolerance off the assigned magnetic heading, with wraparound at 360. Held to the tolerance once turned onto it; before that, a standard-rate-turn allowance. | Gentle → firm → "possible pilot deviation" |
| Speed deviation | IAS above the tighter of 250 kt below 10,000 ft MSL (14 CFR 91.117(a)) and any assigned speed, plus tolerance -- see the heavy-jet exception below | Gentle → firm → "possible pilot deviation" |
| Wrong squawk | The transponder code doesn't match the assigned code, or the mode doesn't report altitude, for longer than a dial-in grace period | Gentle → firm only -- no pilot-deviation step |
A code from 7500, 7600 or 7700 is never flagged as a wrong squawk -- those are handled as emergencies and lost-comms in their own right, not as a conformance deviation. "Doesn't report altitude" means the transponder is off, standby, on (Mode A only, no altitude), or in test mode -- ALT, ground, TA and TA/RA all count as reporting altitude and never trigger this rule on their own, since a real Mode C/S transponder reports pressure altitude in every one of those modes (a modern airliner-style panel normally sits in TA/RA in flight).
Rules run in every airborne phase the engine reaches -- TAKEOFF once off the ground,
DEPARTURE, ENROUTE, DESCENT, APPROACH -- and never on the ground, and never once
in LANDING (see landing conformance below, which takes over at
that point). A new assignment (a different altitude, heading, speed limit, or squawk
code) restarts that rule's ladder. Callouts are spoken from whichever position owns the
aircraft right now: Tower, Departure, or the dynamically created Center/Approach
position once a handoff has actually happened.
A VFR flight bound for another airport (not pattern work) is the one exception: with no clearance to judge it against, none of these rules apply at all until flight following is requested and radar contact is established -- and they stop again the moment service is terminated.
What altitude ATC judges you on¶
A real controller doesn't see the sim's true geometric altitude -- their radar shows
pressure altitude corrected by the local altimeter setting (Mode C), and non-standard
temperature can put that several hundred feet away from true MSL. atc_altitude_ft
(xatc.atc.altitude) computes the altitude the monitor actually uses, from a runtime
altitude source setting:
- Mode C (default): pressure altitude, corrected below 18,000 ft by the local altimeter setting; a raw flight level at or above it.
- Indicated: the pilot's own altimeter, wrong setting and all.
- True altitude: the sim's geometric MSL elevation (the original, pre-setting behavior).
If the sim doesn't report the needed dataref (an old recording, or a sim version that doesn't expose it), this falls back to true MSL and logs once, not on every tick.
Heavy-jet exception¶
14 CFR 91.117(d) lets an aircraft exceed 250 kt below 10,000 ft if its minimum safe
airspeed requires it. With the heavy-speed exception on (the default), a Heavy or
Super-category aircraft (xatc.world.aircraft_types, a curated ICAO-designator table
sourced from FAA JO 7360.1) skips only that 250 kt limit -- an assigned speed is still
enforced regardless of category.
Configuration¶
Altitude source, conformance strictness (relaxed / normal / checkride, same
three levels as the ground monitor, scaling every threshold at once), and the heavy-speed
exception are all runtime settings: adjustable from the Settings
page's ATC tab, persisted, and overridable for a single run
with --altitude-source, --strictness and --no-heavy-speed-exception. A change
applies from the next tick; switching altitude source specifically restarts altitude
tracking so the switch itself can't cause an instant false bust.
Verification¶
53 unit tests cover each rule's capture/before-capture behavior, wraparound, escalation timing, a new assignment restarting the ladder, strictness scaling, altitude-source correctness (including cases where Mode C catches a bust that true MSL would miss, and vice versa), and the heavy-jet exception. A further 7 tests cover the altitude-source helper directly. Unlike the ground monitor, there's no real recorded-flight replay check yet for the airborne rules -- only unit tests against synthetic and replayed states.
Landing conformance¶
xatc.atc.conformance_landing.LandingConformanceMonitor --
src/xatc/atc/conformance_landing.py. Two rules, specific to the moment of landing:
| Rule | Fires when | Ladder |
|---|---|---|
| Landing without clearance | Touching down while arriving, without a landing clearance on file -- also fires for pattern work, where every circuit needs its own landing clearance | Straight to "possible pilot deviation" -- no gentler step first, the same treatment a takeoff without clearance gets |
| Unreported go-around | Descending to within 1,000 ft AGL, then climbing back away from the runway at a real, sustained climb rate, without ever having landed | Straight into the real missed-approach handling -- see below |
Both are spoken from the destination's Tower. The go-around rule's escalation ladder still exists internally (a gentler "say intentions" step before "possible pilot deviation"), but firing it no longer speaks that wording: it's intercepted and sent straight into go-around and missed-approach handling instead, the same "fly the published missed approach" (or a runway-heading climb) treatment a pilot calling "going around" out loud gets -- an unreported go-around is caught and handled exactly like a reported one, not just called out.
Verification¶
9 unit tests cover both rules' trigger conditions and escalation.
Pattern conformance¶
xatc.atc.conformance_pattern.PatternConformanceMonitor --
src/xatc/atc/conformance_pattern.py. Two rules specific to VFR pattern
work, spoken from Tower:
| Rule | Fires when | Ladder |
|---|---|---|
| Pattern altitude | More than the tolerance off field elevation + 1,000 ft AGL (piston) or 1,500 ft (turbine/jet), once established at it this circuit -- suspended from the landing clearance onward, so a normal climb-out or descent to land never fires it | Gentle → firm → "possible pilot deviation" |
| Left the pattern | Farther than the pattern limit from the airport with no call | "say intentions" straight to "possible pilot deviation" -- no gentler step first |
Landing without a clearance in the pattern reuses landing conformance's own rule, not a pattern-specific one.
Airspace conformance¶
xatc.atc.conformance_airspace.AirspaceConformanceMonitor --
src/xatc/atc/conformance_airspace.py. VFR-only rules for entering Class B, C or D
airspace -- IFR flights are never checked, since 14 CFR 91.131/130/129 only bind VFR.
Runs whenever the loaded atc.dat has a Class B, C or D airspace and its controlling
position is staffed, independent of flight phase. See VFR Class B/C/D airspace
entry for the full pilot-facing behavior.
| Rule | Fires when | Ladder |
|---|---|---|
| Class B without clearance | Inside a Class B's lateral boundary without ever having requested/received "cleared into the Class Bravo airspace" | Gentle → firm → "possible pilot deviation" |
| Class B altitude limit | More than the strictness tolerance above a Class B clearance's own "maintain VFR at or below" altitude | "check altitude, maintain VFR at or below..." → "maintain VFR at or below..." → "possible pilot deviation" |
| Class C without contact | Inside a Class C's boundary before the controlling facility has used the aircraft's callsign in a transmission (two-way contact -- no explicit request needed) | Gentle → firm → "possible pilot deviation" |
| Class D without contact | Same as Class C, for a Class D tower's boundary | Gentle → firm → "possible pilot deviation" |
Spoken wording, corrected¶
A phraseology audit against FAA JO 7110.65 caught two real bugs in what these monitors
actually said, both now fixed: the airborne monitor's altitude/heading/speed/squawk
callouts were built from raw numbers instead of routed through phraseology ("climb and
maintain 5000" instead of "climb and maintain five thousand"), and the ground
monitor's off-route-suppression callout spoke a bare taxiway letter ("on taxiway B"
instead of "on taxiway Bravo"). Both are covered by a golden-string test suite
(tests/phraseology/test_golden_phraseology.py) that pins the exact expected wording
for one example of each transmission type, citing the FAA paragraph it follows.
A related event -- no check-in after a handoff -- isn't a rule on any of these
monitors; the engine raises it itself (xatc.atc.conformance_core.RadioRule
.NO_CHECKIN) the same moment it re-transmits a handoff reminder. See Controller
positions & frequencies for that mechanism -- it
feeds the debrief the same way a real conformance event does, without
being one.
Once an emergency, lost-comms code, or hijack code is active, every monitor keeps running and their events still reach the debrief -- but every spoken callout is suppressed, so a distracted pilot handling a real emergency doesn't also get called out for drifting off a taxi route or an altitude.
All five monitors' shared escalation logic (fire once, step up the ladder, reset after
conforming) is factored into one common module, xatc.atc.conformance_core, that
conformance.py, conformance_airborne.py, conformance_landing.py,
conformance_pattern.py and conformance_airspace.py all build on. Readback checking
is a related but separate mechanism from these monitors -- see Readback
checking.