This project models Heathrow as a chain of nine connected operational constraints (not a single capacity number), forecasts demand at 15-minute resolution, converts demand into wait times and breach risk, and answers a specific operational question:
If another 2,000 passengers or 15 flights enter tomorrow's schedule, where does the operation break first — and what is the least disruptive response?
All data is synthetic — generated to be realistic in scale and structure, not sourced from Heathrow Airport Ltd's actual operational data. Where a calibration choice was made (e.g. capacity sizing, service rates), it's documented in code and in this README, not hidden.
src/
config.py Airport structure: terminals, 9 constraints, capacities
pipeline.py Master orchestration — run this to reproduce everything
simulation/
schedule_generator.py Synthetic flight schedule with realistic diurnal demand
demand_model.py Converts flights -> 15-min interval demand per constraint
calibrate.py Reproducible capacity calibration (documents the tuning)
forecasting/
historical_data.py Synthetic historical demand (seasonality + trend + noise)
quantile_forecast.py Hierarchical quantile forecasting (expected/busy/extreme)
queueing/
erlang.py Erlang C multi-server queueing model
wait_time_engine.py Applies queueing across the full demand grid
montecarlo/
shock_simulation.py Weather / delay / staff-absence Monte Carlo simulation
optimization/
staffing_optimizer.py PuLP constraint optimisation — staff reallocation
schedule_shift.py Finds the smallest flight-timing change that resolves a breach
narrative/
bottleneck_detector.py Explainable bottleneck detection & plain-English narrative
stakeholder_negotiation.py Competing stakeholder requests, evaluated against the model
reporting/
excel_workbook.py Capacity-planning workbook (editable assumptions, RAG heatmap,
risk register, scenario comparison — real Excel formulas)
dashboard.py Interactive control-room-style HTML dashboard
briefing.py One-page senior briefing + stakeholder-specific summaries
pdca_cycle.py 4-week continuous improvement (Plan-Do-Check-Act) simulation
| File | What it is |
|---|---|
heathrow_dashboard.html |
Self-contained interactive dashboard — open in any browser |
Heathrow_Capacity_Planning_Workbook.xlsx |
Editable Excel workbook: assumptions, RAG heatmap, risk register, scenario comparison |
senior_leadership_briefing.md |
One-page briefing, every figure traced to model output |
stakeholder_briefings.md |
Separate framing for airlines, security, finance |
pdca_four_week_log.csv |
Simulated 4-week forecast-vs-actual improvement cycle |
The capacity-planning workbook ships with live formulas (Quick Capacity Check recalculates as you edit assumptions), conditional-formatted RAG heatmaps, a ranked risk register, and a baseline-vs-stress scenario comparison. Verified zero formula errors via LibreOffice recalculation.
Assumptions — editable scenario levers

Quick Capacity Check — live formula recalculation

RAG Capacity Heatmap — peak utilisation by hour, every terminal/process

Operational Risk Register — ranked by severity score

Scenario Comparison — baseline vs stress test

pip install numpy pandas scipy scikit-learn statsmodels plotly openpyxl networkx pulp
python -m src.pipeline # runs baseline + stress scenario (~65s)
python -c "import pickle; from src.pipeline import run_baseline_and_stress; \
pickle.dump(run_baseline_and_stress(mc_trials=150), open('cache/scenario_results.pkl','wb'))"
python src/reporting/excel_workbook.py
python src/reporting/dashboard.py
python src/reporting/briefing.py
python src/reporting/pdca_cycle.py # ~2 min — refits forecaster 4xpip install pytest
python -m pytest tests/ -v10 tests covering: demand mass-conservation, non-negativity, Erlang C queueing correctness (wait time monotonicity, instability, resource sensitivity), and quantile-forecast ordering (expected ≤ busy ≤ extreme, never negative).
The demand model
Flights don't show up as passengers all at once, they trickle in. If your flight's at 6:30am, you're not all walking up to check-in at 6:30am, you're showing up between 4:00 and 5:00. So instead of dumping passenger counts straight onto a flight time, I built "offset profiles" for each of the 9 constraints (check-in, security, border control, baggage, stands, ground handling, PRM assistance, and the two runway processes): little curves that say this many people show up this many minutes before/after the flight.
I also added a realistic aircraft mix (regional/narrowbody/widebody, each with their own typical load factor) and terminal shares roughly matching real Heathrow proportions, because "every flight has 150 people" felt lazy.
One thing I'm actually proud of: I checked that passengers don't get created or lost in this whole spreading-out process. Total check-in demand should equal exactly (departure passengers × the fraction who actually need a desk instead of just doing it on their phone). It matched to the decimal. Small thing, but it's the kind of bug that's very easy to introduce and very annoying to notice later.
The time I broke the entire airport
So here's a fun one. My first pass at capacity numbers left basically everything over capacity, some processes were running at 460% utilisation. Four. Sixty. Percent. The airport was just... permanently on fire, all day, every day.
Which is a problem, because the whole point of this project is "show me where it breaks first." If everything's already broken, that question doesn't mean anything anymore.
Turned out I'd made two dumb assumptions: I had border control staffed like it was 1995 (nobody using e-gates), and I assumed everyone physically queues at check-in, when in reality most people check in on their phone and only show up at a desk if they're dropping a bag. Fixed both, and suddenly the baseline runs tight, like 82-95% utilisation, which is what a real busy airport actually feels like, with runway capacity as the one thing that's genuinely maxed out.
Which, kind of amusingly, is also true of the real Heathrow. It only has two runways and that's famously its biggest constraint. I didn't plan that, it just fell out of doing the modelling properly. I'll take it.
Forecasting
Gradient-boosted quantile regression (via scikit-learn) trained on day-of-week, time-of-day, terminal, process, and what demand looked like 7 days ago. It spits out three numbers per cell: expected, busy, and extreme (P50/P90/P99, if you like your stats formal). I double-checked that "extreme" is never lower than "expected," because quantile models can absolutely do that to you if you're not careful, and it would be pretty embarrassing.
Queueing
Erlang C, basically the actual maths for "if this many people arrive per hour and I have this many desks, how long do they wait." I treat each 15-minute window as its own little steady-state queue, which is the standard simplification everyone in this space uses because modeling the true second-by-second queue dynamics is a nightmare nobody has time for. Runways get their own simpler treatment since they're one shared airport-wide resource, not a "6 lanes, pick one" situation.
Monte Carlo
I run 150-200 randomized "what if tomorrow actually goes like this" simulations: bad weather, a wave of delayed flights, a chunk of staff calling in sick, and see how often each part of the airport breaches. So instead of saying "security will be over capacity," the model can say "there's a 91% chance security breaches between 6:45 and 7:30," which is a much more useful thing to tell a human.
The two "what do we actually do about it" tools
I built two: one reallocates a fixed pool of staff hours toward the worst moments of the day (using actual constrained optimization, via PuLP, not vibes), and one searches for the smallest possible fix, literally "what's the fewest flights I can nudge, by the fewest minutes, to make this breach go away." That second one is the one that produces sentences like "moving 3 flights by 25 minutes clears the breach," which is exactly the style of answer this whole project was supposed to produce.
Stakeholders arguing with each other
When an airline asks for more slots, I don't script the answer, I actually run it through the model and see what breaks. Most of the time it's runway capacity, which is a hard wall no amount of extra check-in staff can fix. Where it is fixable (security, check-in, assistance), the model proposes the smallest real fix instead of just saying no.
The thing I want to be upfront about, not bury in a footnote
In the 4-week improvement loop, some of the "worst gap" numbers (20-25%) are really just noise from small sample sizes, like if only 4 aircraft are using a stand that hour, one extra flight swings the percentage a lot even though nothing meaningful actually happened. It's not fake, but it's also not a real signal you should act on without a human glancing at it first. I'd rather say that plainly than have someone find it themselves and wonder what else I glossed over.

