Skip to content

Airline operations

schedule

Flight schedules: flights, aircraft rotations, crew pairings and passenger connections.

Times are minutes from midnight of the operating day. CSV files may give times as HH:MM (hours may exceed 23 for after-midnight arrivals), as minutes, or as ISO date-times (2026-10-08T06:30), which are converted relative to midnight of the earliest date in the file.

Flight dataclass

Flight(id: str, tail: str, origin: str, dest: str, std: float, sta: float, pax: int = 150, crew: str = '', aircraft_type: str = '', seats: int = 0)

One scheduled flight leg.

Connection dataclass

Connection(inbound: str, outbound: str, pax: int)

pax passengers connecting from flight inbound to flight outbound.

Schedule

Schedule(flights: Iterable[Flight], connections: Iterable[Connection] = ())

A day of flights, with rotations (by tail), crew pairings and connections.

s = Schedule([ ... Flight("A1", "T1", "HUB", "AAA", 360, 450), ... Flight("A2", "T1", "AAA", "HUB", 495, 585), ... ]) ", ".join(f.id for f in s.rotations["T1"]) 'A1, A2'

validate

validate() -> list[str]

Problems that make the schedule unflyable (empty list when consistent).

replace_flights

replace_flights(flights: Iterable[Flight]) -> Schedule

A schedule with flights and the connections that still refer to them.

retime

retime(shifts: Mapping[str, float]) -> Schedule

Move flights by {flight_id: minutes} (positive = later).

from_csv classmethod

from_csv(path: str | Path, connections: str | Path | None = None, *, default_pax: int = 150) -> Schedule

Load a schedule CSV (and optionally a connections CSV: inbound, outbound, pax).

synthetic classmethod

synthetic(*, hub: str = 'HUB', spokes: Sequence[str] = ('AAA', 'BBB', 'CCC', 'DDD', 'EEE', 'FFF', 'GGG', 'HHH'), tails: int = 12, first_departure: float = 360.0, last_departure: float = 1260.0, turn: float = 45.0, banks: bool = True, bank_spacing: float = 120.0, crews_per_day: int = 2, connect_share: float = 0.3, seed: int = 0) -> Schedule

A hub-and-spoke day: each tail flies hub-spoke round trips.

With banks the hub departures cluster in waves bank_spacing apart (good connections, peaky runway load); without, they are spread evenly. Crews fly half-day pairings; a share of passengers on each inbound connects to the next hub departures.

parse_time

parse_time(value: Any, base: datetime | None = None) -> float

Minutes from midnight: "06:30" -> 390, "25:10" -> 1510, 95 -> 95.

format_time

format_time(minutes: float) -> str

390 -> "06:30"; times past midnight keep counting ("25:10").

config

Operating assumptions for the airline network simulation.

:class:OpsConfig holds the rules of the day (turn times, runway rates, gates, curfews, crew duty limits, spares, standby crews, recovery thresholds, costs). :class:DelayModel says how much primary delay (technical, ATC, late passengers - anything not caused by an earlier flight) each departure gets and how variable block times are; it can be fitted to history with :func:simulsi.aviation.fit_delay_model. :class:WeatherEvent cuts an airport's departure rate for a time window, with a probability, so forecasts can carry weather scenarios.

DelayModel dataclass

DelayModel(prob: float = 0.2, mean: float = 25.0, block_cv: float = 0.06, table: dict[str, tuple[float, float]] = dict(), block_bias: dict[str, float] = dict(), block_scale: float = 1.0, shape: list[float] = list(), day_sigma: float = 0.0, airport_days: dict[str, list[float]] = dict(), cancel_rate: float = 0.0, cancel_days: dict[str, list[float]] = dict(), predictor: DelayPredictor | None = None)

Primary departure delays and block-time variability.

Each departure gets a primary delay with probability prob; its size is exponential with mean mean minutes. table overrides both per origin airport ("LHR") or per origin and hour ("LHR@07"), and block_bias scales the scheduled block time per route ("LHR-CDG"). predictor - for example a machine-learning model - returns the expected primary delay of a flight; the simulation then samples around that expectation and propagates it through the network.

shape replaces the exponential with an empirical distribution: equally spaced quantiles of primary delay divided by their mean (so mean still sets the size). Real delays have heavier tails than an exponential; :func:~simulsi.aviation.fit_delay_model fills it in. day_sigma makes whole days better or worse together (weather, ATC programmes): each simulated day draws a lognormal factor on every delay chance. airport_days does the same per airport from the empirical spread of good and bad days there (quantiles with mean 1; key "*" for any airport). cancel_rate is the chance a departure is cancelled for reasons outside the model (weather, crew, mechanical), scaled up on bad days; cancel_days replaces it with the empirical spread of each airport's daily cancelled share, drawn together with the delay factor.

airport_factors

airport_factors(airports: Sequence[str], rs: RandomStream) -> tuple[dict[str, float], dict[str, float]]

How bad the day is at each airport, drawn from history: (delay factor, cancelled share).

One draw per airport sets both, so a bad delay day is also a bad cancellation day.

day_factor

day_factor(rs: RandomStream) -> float

How bad the whole day is: multiplies every flight's delay chance (mean 1).

WeatherEvent dataclass

WeatherEvent(airport: str, start: float, end: float, capacity: float = 0.4, probability: float = 1.0, name: str = '', cancel: float = 0.0)

Departure capacity at airport falls to capacity x normal from start to end.

probability is the chance the event happens on the day (each replication draws it); capacity=0 closes the airport to departures. cancel is the share of departures in the window the airline cancels in advance (as for a forecast winter storm).

parse classmethod

parse(text: str) -> WeatherEvent

"HUB 15:00-18:00 0.4 p=0.6" (capacity and probability optional).

OpsConfig dataclass

OpsConfig(on_time: float = 15.0, min_turn: float = 35.0, min_turn_by_airport: dict[str, float] = dict(), late_turn_compression: float = 1.0, runway_rate: dict[str, float] = dict(), weather_base_rate: float = 30.0, gates: dict[str, int] = dict(), curfew: dict[str, float] = dict(), min_crew_connect: float = 30.0, duty_limit: float = 13 * 60.0, report_time: float = 60.0, release_time: float = 30.0, standby_callout: float = 60.0, spares: dict[str, int] = dict(), standby_crews: dict[str, int] = dict(), swap_threshold: float = 90.0, cancel_threshold: float = 240.0, mct: float = 35.0, load_factor: float = 0.85, overnight_delay: float = 18 * 60.0, spare_ferry_minutes: float | None = None, delay_cost_per_minute: float = 100.0, cancel_cost: float = 20000.0, misconnect_cost_per_pax: float = 250.0, eu261: bool = False, delays: DelayModel = DelayModel(), otp_recalibration: tuple[float, float] = (0.0, 1.0))

Rules and resources for one day of operations.

Per-airport settings are dictionaries keyed by airport code; airports not listed are unconstrained (no runway queue, unlimited gates, no curfew, no spares or standby crews).

thunderstorm

thunderstorm(airport: str, start: str | float, hours: float = 3.0, probability: float = 1.0) -> WeatherEvent

Convective weather: departure rate cut to 40 %.

morning_fog

morning_fog(airport: str, until: str | float = '10:00', probability: float = 1.0) -> WeatherEvent

Low visibility procedures from 05:00: departure rate cut to 60 %.

snow

snow(airport: str, start: str | float = '06:00', hours: float = 6.0, probability: float = 1.0) -> WeatherEvent

Snow and de-icing: departure rate cut to 50 % for a long window.

closure

closure(airport: str, start: str | float, minutes: float, probability: float = 1.0) -> WeatherEvent

Full closure to departures (storm, runway incident, ATC strike).

weather_from_config

weather_from_config(data: Mapping[str, Any]) -> list[WeatherEvent]

The weather: list of an ops YAML file (strings or mappings).

state

The live operation: what has already happened, and the actions controllers can take.

:class:OpsState captures the day so far - actual departure and arrival times, known delays (estimated departure times), cancellations and aircraft out of service - so a forecast can start from now instead of from the plan. Actions (:class:Cancel, :class:Retime, :class:Swap) change the plan; :func:apply_actions returns the changed schedule so alternatives can be simulated side by side.

FlightStatus dataclass

FlightStatus(atd: float | None = None, ata: float | None = None, etd: float | None = None, cancelled: bool = False)

What is known about one flight: actual or estimated times, or a cancellation.

OpsState dataclass

OpsState(now: float, flights: dict[str, FlightStatus] = dict(), aircraft_unavailable: dict[str, float] = dict())

The operation at time now (minutes from midnight).

from_records classmethod

from_records(now: float | str, rows: Iterable[Mapping[str, Any]] = (), aircraft_unavailable: Mapping[str, float | str] | None = None) -> OpsState

Rows with flight and any of atd, ata, etd, status (cancelled).

load classmethod

load(source: str, now: float | str | None = None) -> OpsState

Live status from a CSV file, or from an http(s) URL serving CSV or JSON.

JSON may be a list of status rows or {"now": "13:05", "flights": [...]}. now defaults to the feed's now, else the local clock.

to_csv

to_csv(path: str | Path | None = None) -> str

The flight statuses as CSV (flight, atd, ata, etd, status), readable by :meth:from_csv.

Cancel dataclass

Cancel(*flights: str)

Cancel flights (cancel a round trip to keep the aircraft where it is).

Retime dataclass

Retime(flight: str, minutes: float)

Delay a flight by minutes on purpose (later flights of the tail move only if they must).

Swap dataclass

Swap(tail_a: str, tail_b: str, after: float)

Swap the remaining rotations of two aircraft from after (minutes) onwards.

Both aircraft must be at the same airport at the swap point.

apply_actions

apply_actions(schedule: Schedule, actions: Iterable[Action]) -> tuple[Schedule, set[str]]

The schedule after actions, and the set of cancelled flight ids.

parse_action

parse_action(text: str) -> Action

"cancel F101 F102", "retime F101 30", "swap T01 T02 12:00".

engine

Simulate one day of an airline network: aircraft rotations, crews, runways, gates and recovery.

Each aircraft (tail) works through its rotation as a simulsi process. A departure waits for the aircraft (inbound arrival plus minimum turn), the crew (its previous flight plus a connection time) and any known estimate, then picks up a sampled primary delay and queues for a departure slot (only at airports with a runway rate or weather; arrivals there queue for landing slots too, and wait out closures). About an hour before each departure, operations control looks at the projected delay and may

  • swap in a spare aircraft at the airport (the late aircraft becomes the spare),
  • call a standby crew (late inbound crew, or a crew that would break its duty limit),
  • cancel a round trip when the projected delay passes cancel_threshold,
  • cancel the rest of a rotation that would break a curfew.

Arrivals may wait for a gate at airports with a gate limit. Passenger connections are checked after the day: a connection is missed when either flight is cancelled or the outbound leaves less than mct minutes after the inbound arrives.

The engine is deterministic for a seed, and every flight draws its random primary delay, block time and turn time from its own position in a fixed order, so two plans (say, with and without an action) see the same disturbances - common random numbers, which makes comparisons sharp.

FlightOutcome dataclass

FlightOutcome(id: str, tail: str, dep: float = math.nan, arr: float = math.nan, cancelled: bool = False, reason: str = '', dep_delay: float = math.nan, arr_delay: float = math.nan, primary: float = 0.0, reactionary_aircraft: float = 0.0, reactionary_crew: float = 0.0, runway: float = 0.0, gate_wait: float = 0.0, spare: bool = False, standby: bool = False, crew_risk: bool = False)

What happened to one flight in one simulated day.

DayOutcome dataclass

DayOutcome(flights: dict[str, FlightOutcome], misconnected: dict[tuple[str, str], int], metrics: dict[str, float], weather: list[str] = list())

One simulated day: per-flight outcomes, missed connections and network metrics.

build_day

build_day(sim: Simulation, schedule: Schedule, config: OpsConfig, *, weather: Sequence[WeatherEvent] = (), state: OpsState | None = None, cancelled: Iterable[str] = (), out: dict[str, Any] | None = None) -> None

Add one day of operations to sim; outcomes land in out when the run finishes.

rebook

rebook(schedule: Schedule, cfg: OpsConfig, outcomes: Mapping[str, FlightOutcome], misconnected: Mapping[tuple[str, str], int]) -> dict[str, float]

Re-accommodate disrupted passengers on later direct flights with free seats.

Misconnected passengers and the local passengers of cancelled flights are placed, earliest need first, on the first operated flight from where they are to where they are going that leaves after they are ready and still has seats (seats, or pax / load_factor when a flight has no seat count). Whoever finds no seat today is stranded until tomorrow (overnight_delay minutes).

simulate_day

simulate_day(schedule: Schedule, config: OpsConfig | None = None, *, seed: int | None = 0, weather: Sequence[WeatherEvent] = (), state: OpsState | None = None, actions: Sequence[Action] = ()) -> DayOutcome

Simulate one day and return per-flight outcomes and network metrics.

from simulsi.aviation import Schedule day = simulate_day(Schedule.synthetic(tails=4), seed=1) 0 <= day.metrics["otp"] <= 1 True

network_model

network_model(schedule: Schedule, config: OpsConfig | None = None, *, weather: Sequence[WeatherEvent] = (), hub: str | None = None) -> Model

The day as a simulsi :class:~simulsi.Model, for experiments, optimisation and Pareto search.

Parameters: spares and standby_crews (at the hub), swap_threshold, cancel_threshold, min_turn, delay_scale (multiplies the chance of a primary delay) and weather (0/1).

forecast

Monte Carlo forecasts of a day of operations: per flight, per rotation, per connection.

:func:forecast simulates the day many times (from the plan, or from the live state at now) and turns the outcomes into probabilities and delay ranges: the chance each flight is on time or cancelled, its departure delay quantiles, the expected passengers who misconnect, which aircraft rotations are fragile, and alerts when risks cross thresholds.

Forecast

Forecast(schedule: Schedule, config: OpsConfig, days: Sequence[DayOutcome], *, now: float | None = None, state: OpsState | None = None, weather: Sequence[WeatherEvent] = (), cancelled: Sequence[str] = ())

Per-flight and network forecasts from replications simulated days.

summary

summary() -> dict[str, dict[str, float]]

Network metrics: mean and 10th/90th percentiles over the simulated days.

rotations

rotations() -> list[dict[str, Any]]

Aircraft rotations ranked by risk (expected delay minutes plus cancellations).

alerts

alerts(*, cancel: float = 0.3, delay_p80: float = 60.0, crew: float = 0.3, misconnect: float = 0.5, min_pax: int = 10) -> list[Alert]

Warnings for flights and connections whose risks cross the thresholds.

gantt

gantt() -> list[dict[str, Any]]

Bars for a tail-by-time chart: planned times plus predicted median delay.

forecast

forecast(schedule: Schedule, config: OpsConfig | None = None, *, replications: int = 200, seed: int = 0, weather: Sequence[WeatherEvent] = (), state: OpsState | None = None, actions: Sequence[Action] = ()) -> Forecast

Simulate the day replications times and summarise per flight and per network.

Pass state to forecast the rest of the day from the live situation, and actions to forecast a changed plan. The same seed gives the same disturbances, so two forecasts with different actions compare like with like.

from simulsi.aviation import Schedule fc = forecast(Schedule.synthetic(tails=4), replications=20) otp = fc.summary()["otp"] 0 <= otp["mean"] <= 1 True

state_at

state_at(schedule: Schedule, day: DayOutcome, now: float) -> OpsState

The live state at now of a simulated (or real) day: what had departed, landed or been cancelled.

Useful to test rolling forecasts against a known "truth" day.

rolling_forecast

rolling_forecast(schedule: Schedule, config: OpsConfig | None = None, *, truth: DayOutcome, times: Sequence[float], replications: int = 100, seed: int = 0, weather: Sequence[WeatherEvent] = ()) -> list[dict[str, Any]]

Re-forecast through a day as it unfolds, and score each forecast against what happened.

truth is the day that "really" happened (a simulated day or real actuals). For each time in times the forecast starts from the state at that time; the rows report the predicted OTP against the actual one and the mean absolute error of predicted arrival delays.

recovery

Operations control: compare recovery options and search for a good set of actions.

Every option is simulated with the same random disturbances (common random numbers), so differences between options come from the actions, not from luck. Costs combine delay minutes, cancellations, misconnected passengers and (optionally) EU261 compensation - see :class:OpsConfig.

PlanComparison dataclass

PlanComparison(names: list[str], samples: dict[str, dict[str, ndarray]], actions: dict[str, list[Action]])

Mean metrics per option and the paired difference in cost against the first option.

compare_plans

compare_plans(schedule: Schedule, options: Mapping[str, Sequence[Action]], config: OpsConfig | None = None, *, replications: int = 100, seed: int = 0, weather: Sequence[WeatherEvent] = (), state: OpsState | None = None, include_baseline: bool = True) -> PlanComparison

Simulate each option (a list of actions) on the same disturbances and compare.

from simulsi.aviation import Schedule, Cancel s = Schedule.synthetic(tails=4) cmp = compare_plans(s, {"cancel": [Cancel("F100", "F101")]}, replications=10) cmp.names ['as planned', 'cancel']

candidate_actions

candidate_actions(schedule: Schedule, fc: Forecast, *, now: float | None = None, max_flights: int = 8, hold_minutes: Sequence[float] = (15.0,)) -> list[Action]

Plausible controller actions for the riskiest flights in a forecast.

  • cancel the round trip of a badly delayed flight,
  • swap the rest of a late aircraft's rotation with an aircraft that is on the ground at the same airport and expected to be on time,
  • hold a departure for a large group of connecting passengers.

recover

recover(schedule: Schedule, config: OpsConfig | None = None, *, state: OpsState | None = None, weather: Sequence[WeatherEvent] = (), candidates: Sequence[Action] | None = None, max_actions: int = 3, replications: int = 60, seed: int = 0, min_saving: float = 0.0) -> RecoveryResult

Greedy search for a few actions that lower the expected disruption cost.

Each round tries every remaining candidate on top of the actions chosen so far (same disturbances for all), keeps the one that saves most, and stops when the best saving is not clearly above noise (two standard errors of the paired difference) or below min_saving, or after max_actions actions. Candidates default to :func:candidate_actions.

planning

Planning before the day (and months ahead): reserves, schedule buffers, schedule changes.

plan_reserves

plan_reserves(schedule: Schedule, config: OpsConfig | None = None, *, airport: str | None = None, spares: Sequence[int] = (0, 1, 2, 3), standby_crews: Sequence[int] = (0, 1, 2, 3), spare_cost: float = 15000.0, standby_cost: float = 2500.0, weather: Sequence[WeatherEvent] = (), replications: int = 60, seed: int = 0) -> ReservePlan

How many spare aircraft and standby crews to hold at airport (default: the hub).

Every combination is simulated on the same disturbances; the total cost adds the daily cost of the reserves to the expected disruption cost. The Pareto front marks combinations where no other one has both a lower reserve cost and a lower expected disruption cost.

optimize_buffers

optimize_buffers(schedule: Schedule, config: OpsConfig | None = None, *, budget: float = 60.0, step: float = 5.0, per_tail_limit: float | None = None, replications: int = 100, seed: int = 0, weather: Sequence[WeatherEvent] = ()) -> BufferPlan

Spend budget minutes of extra ground time where it stops the most delay.

Simulates the plan, records how late each aircraft was for each turn (knock-on delay), and adds buffer in step-minute pieces to the turn with the largest expected benefit: the knock-on delay a piece absorbs times the number of legs it protects (that leg and the rest of the rotation). Later legs of the tail move later by the added buffer. The new schedule is then simulated on the same disturbances.

schedule_impact

schedule_impact(base: Schedule, changed: Schedule, config: OpsConfig | None = None, *, names: tuple[str, str] = ('base', 'changed'), replications: int = 100, seed: int = 0, weather: Sequence[WeatherEvent] = ()) -> ScheduleImpact

Compare two schedules (an added rotation, a new bank structure, retimed flights).

Flights with the same id see the same random disturbances in both, so the difference shows the effect of the change itself.

calibration

Calibrate on history and check forecasts against what really happened.

:class:History holds actual flights (scheduled and actual times, tails, cancellations) for many days - from a generic CSV or the US BTS on-time dataset. :func:fit_delay_model separates primary delay (what the departure added on its own) from knock-on delay of a late inbound aircraft, and estimates how often and how much primary delay happens per airport and hour. :func:backtest forecasts each historical day from its schedule and scores the forecasts: whether flights given a 70 % on-time chance really are on time about 70 % of the time (reliability), the Brier score, delay-quantile coverage and the error of the predicted OTP.

History dataclass

History(flights: list[ActualFlight] = list(), tz_offsets: dict[str, float] = dict(), clock: str = '')

Actual flights over several days.

When the file has scheduled block times (BTS CRSElapsedTime), local clock times are converted to one clock - the easternmost airport's, or clock - using UTC offsets inferred from the data itself (tz_offsets, minutes relative to that clock), so rotations through several time zones line up.

schedule

schedule(date: str, *, repair: bool = True) -> Schedule

The day's schedule. With repair, a tail whose recorded legs do not chain (a diverted or missing flight, overlapping times) is split into separate rotations (N123#2) at each break, so the schedule always validates.

state

state(date: str, now: float) -> OpsState

What was known at now on date: departures, arrivals and cancellations so far.

causes

causes() -> dict[str, Any]

Where the delay minutes and cancellations come from (BTS cause columns).

from_records classmethod

from_records(rows: Iterable[Mapping[str, Any]], *, carrier: str | None = None, clock: str | None = None) -> History

Rows with date, flight, tail, origin, dest, std, sta, atd/ata (or dep_delay/arr_delay), cancelled.

Times may be HH:MM, minutes or HHMM (BTS style, e.g. 0730). carrier keeps one airline's flights (BTS files hold all of them).

from_csv classmethod

from_csv(path: str | Path, *, carrier: str | None = None, clock: str | None = None) -> History

A history CSV, including US BTS on-time files (both the download-form names such as FL_DATE, TAIL_NUM, CRS_DEP_TIME and the monthly PREZIP names such as FlightDate, Tail_Number, CRSDepTime). carrier filters while reading. A .zip (as downloaded from BTS) is read directly.

concat classmethod

concat(parts: Sequence[History]) -> History

Several histories (e.g. months) as one; time-zone offsets from the first that has them.

simulated classmethod

simulated(schedule: Schedule, config: OpsConfig | None = None, *, days: int = 30, seed: int = 0, start: str = '2026-01-01') -> History

A synthetic history: days simulated days of schedule (for testing calibration).

BacktestResult dataclass

BacktestResult(days: list[dict[str, Any]], reliability: list[dict[str, Any]], brier: float, brier_climatology: float, coverage: dict[str, float], otp_mae: float, live: list[dict[str, Any]] = list())

skill property

skill: float

Brier skill score against always predicting the overall on-time rate (> 0 is better).

fit_delay_model

fit_delay_model(history: History, *, min_turn: float = 35.0, by: str = 'origin', min_samples: int = 30, threshold: float = 1.0, empirical: bool = True, min_days: int = 10) -> DelayModel

Estimate primary-delay rates and block-time variability from history.

A departure's knock-on delay is how late its aircraft was ready (the actual arrival of the tail's previous flight that day plus min_turn); the rest of its departure delay is primary. Primary delays above threshold minutes give the probability, and their mean the exponential mean, per origin (by="origin") or per origin and hour (by="origin_hour") when there are at least min_samples departures, else network-wide. Block-time variability and per-route bias come from actual against scheduled block times.

fit_late_turns

fit_late_turns(history: History, turns: Mapping[str, float], default: float = 35.0) -> float

How much ground staff speed up a late aircraft's turn, as OpsConfig.late_turn_compression.

Compares the actual ground time of turns where a late inbound held up the next departure with the fitted minimum turn: 0 means late turns always hit 0.9 x the minimum, 1 means they are no faster than usual.

fit_turn_times

fit_turn_times(history: History, *, quantile: float = 0.1, min_samples: int = 20, floor: float = 15.0) -> dict[str, float]

Minimum turn time per airport: a low quantile of the actual ground times between consecutive legs of the same aircraft (the fastest turns the operation achieves).

calibrate

calibrate(history: History, config: OpsConfig | None = None, *, iterations: int = 6, replications: int = 10, max_days: int = 7, seed: int = 0, late: float = 15.0, day_effect: bool = True) -> DelayModel

Fit primary delays so that simulated departures look like history.

:func:fit_delay_model gives a first guess, but it counts everything that is not knock-on delay as primary - including runway queues and crew waits that the simulation adds by itself. This function re-simulates up to max_days historical schedules with config and nudges each airport's primary-delay probability and mean until the simulated share of departures more than late minutes late and the mean departure delay match the history (a simple method-of-simulated-moments loop). With day_effect it then sets day_sigma so simulated days vary as much as real ones do.

fit_recalibration

fit_recalibration(history: History, config: OpsConfig, *, dates: Sequence[str] | None = None, replications: int = 30, seed: int = 0) -> tuple[float, float]

Platt scaling of on-time probabilities: logit(p') = a + b logit(p), fitted on past days.

Simulated probabilities can be systematically too sure (or not sure enough) - for example when real controllers recover late aircraft in ways the model does not know. Fit on training days, then set OpsConfig.otp_recalibration so forecasts report corrected chances.

reliability_table

reliability_table(probabilities: Sequence[float], outcomes: Sequence[bool], bins: int = 10) -> list[dict[str, Any]]

Calibration of probability forecasts: predicted against observed frequency per bin.

backtest

backtest(history: History, config: OpsConfig | None = None, *, replications: int = 100, seed: int = 0, dates: Sequence[str] | None = None, live_at: Sequence[float] = ()) -> BacktestResult

Forecast each historical day and score the forecasts against what happened.

Day-ahead forecasts use the schedule alone. For each time in live_at (minutes on the history's clock) the day is also re-forecast from what was known at that time, and the flights still to depart are scored - showing how much the live state adds.

ground

Ground operations: the turnaround critical path, minimum turn times and minimum connection times.

NARROWBODY module-attribute

NARROWBODY = (TurnaroundTask('deboard', 6, 8, 12), TurnaroundTask('unload_bags', 8, 10, 15), TurnaroundTask('clean', 7, 10, 15, ('deboard',)), TurnaroundTask('cater', 6, 8, 14, ('deboard',), 0.05, 10), TurnaroundTask('fuel', 8, 12, 18, ('deboard',), 0.05, 12), TurnaroundTask('load_bags', 8, 11, 16, ('unload_bags',)), TurnaroundTask('board', 12, 15, 24, ('clean', 'cater', 'fuel'), 0.1, 6), TurnaroundTask('close_doors', 2, 3, 5, ('board', 'load_bags')), TurnaroundTask('pushback', 2, 3, 6, ('close_doors',)))

A typical A320/737 turnaround (minutes), fuelling with passengers off.

TurnaroundTask dataclass

TurnaroundTask(name: str, low: float, mode: float, high: float, after: tuple[str, ...] = (), disruption: float = 0.0, disruption_mean: float = 10.0)

A ground task with a triangular duration (minutes) that starts once after are done.

disruption is the chance the task overruns, by an exponential extra time with mean disruption_mean (a late fuel truck, a missing caterer, a slow wheelchair assist).

TurnaroundResult dataclass

TurnaroundResult(totals: ndarray, criticality: dict[str, float], mean_duration: dict[str, float], mean_slack: dict[str, float], tasks: tuple[TurnaroundTask, ...] = ())

min_turn

min_turn(reliability: float = 0.95) -> float

The scheduled turn time that is long enough on reliability of days.

turnaround

turnaround(tasks: Sequence[TurnaroundTask] = NARROWBODY, *, replications: int = 2000, seed: int = 0, crews: dict[str, int] | None = None) -> TurnaroundResult

Simulate a turnaround as a network of ground tasks and find the critical path.

Each task is a simulsi process that waits for its predecessors, then takes a sampled duration. crews optionally limits shared teams ({"clean": 1} means cleaning can only start when the cleaning team is free - useful for banked schedules). The criticality index of a task is the share of turns where it lies on the longest (critical) path.

r = turnaround(replications=200) 35 < r.quantile(0.5) < 60 True

recommend_min_turn

recommend_min_turn(tasks: Sequence[TurnaroundTask] = NARROWBODY, *, reliability: float = 0.95, replications: int = 2000, seed: int = 0) -> float

Shortest scheduled turn that is achieved on reliability of turns.

recommend_mct

recommend_mct(*, reliability: float = 0.95, walk: tuple[float, float, float] = (8.0, 12.0, 25.0), gate_close: float = 15.0, inbound_delay: tuple[float, float] = (0.25, 20.0), outbound_delay: tuple[float, float] = (0.2, 15.0), inbound_samples: Sequence[float] | None = None, outbound_samples: Sequence[float] | None = None, samples: int = 20000, seed: int = 0, candidates: Sequence[float] = (25, 30, 35, 40, 45, 50, 60, 75, 90)) -> MCTResult

The minimum connection time that reliability of passengers make.

A passenger makes the connection when the inbound arrival delay, plus the walk (triangular walk minutes) and the gate closing gate_close minutes before departure, fits in the scheduled connection plus the outbound's own delay. Delays are "probability, mean" (exponential), or empirical samples - for example arrival delays from a :class:~simulsi.aviation.Forecast.

passengers

Passenger-side decisions: compensation exposure, overbooking and check-in staffing.

StaffingPlan dataclass

StaffingPlan(slot_minutes: float, start: float, arrivals: list[float], staff: list[int], expected_wait: list[float], simulated_wait: list[float])

Counters per time slot for a check-in or security area.

eu261_amount

eu261_amount(block_minutes: float) -> float

EU261 compensation per passenger (EUR), with block time standing in for distance.

Short-haul (up to ~1 500 km, about 2.5 h) EUR 250, medium (up to ~3 500 km, about 4.5 h) EUR 400, long-haul EUR 600.

eu261_compensation

eu261_compensation(flight: Flight, arrival_delay: float, cancelled: bool) -> float

Compensation owed for one flight: arrivals 3 h+ late, or short-notice cancellations.

Extraordinary circumstances (weather, ATC) are not modelled, so this is an upper bound on exposure.

overbooking

overbooking(capacity: int, show_rate: float, *, fare: float = 150.0, denied_boarding_cost: float = 600.0, max_extra: int | None = None) -> OverbookingResult

The booking limit that maximises expected revenue minus denied-boarding cost.

Passengers show up independently with probability show_rate. For each limit from capacity upwards the table gives expected empty seats, expected denied boardings, the chance of denying anyone and the expected net revenue.

overbooking(180, 0.92).best_limit > 180 True

passenger_arrivals

passenger_arrivals(flights: Sequence[Flight], *, slot_minutes: float = 15.0, earliest: float = 180.0, latest: float = 45.0, peak: float = 100.0) -> tuple[float, list[float]]

Expected check-in arrivals per slot: each passenger turns up between earliest and latest minutes before departure (triangular, most likely peak).

checkin_staffing

checkin_staffing(flights: Sequence[Flight], *, service_minutes: float = 2.5, target_wait: float = 10.0, slot_minutes: float = 15.0, max_staff: int = 60, seed: int = 0, replications: int = 5, corrections: int = 8) -> StaffingPlan

Counters needed per slot to keep the mean wait under target_wait minutes.

Staffing per slot starts from the Erlang C formula on that slot's arrival rate (the usual "stationary independent period" method). Queues carry over from busy slots into quiet ones, which that method misses, so a simulation with the time-varying staffing then checks every slot and adds counters where (and just before where) the simulated wait is too long, up to corrections rounds. expected_wait is the Erlang C figure for the first guess, simulated_wait the final check.