Story: one continuous line from a single engine shop visit to the company’s future
A summary of the work as of September 2026. In reading order: the question → what we built → what we learned → the next decision.
All figures are synthetic data unless marked “real data”. Sources and confidence levels are recorded in the model’s input data.
1. Starting point: what we set out to do was engine planning
The plan was about deciding, within a 2-year window, “which engine, when, and with which workscope”. But the question maintenance planners actually carry is longer. How many more shop visits will this aircraft’s engines need, and what should each visit restore so that they last just until retirement? Which aircraft should leave the fleet first so that the least remaining life is thrown away?
What we built: a shop-visit sequence through to retirement (runout.py). Inside the window it uses the frozen plan; after the window it takes each aircraft’s state left by the 20-year simulation, flies it to retirement at average deterioration, and when a limit is reached picks “the lightest workscope that lasts until retirement”. It compares three retirement-order policies.
What we learned for Carrier A (synthetic data): 56 shop visits through the final retirement in Mar 2036, with a lifetime cost of $487M. Retiring “aircraft whose next shop visit is nearest first” gives 56 visits; a random order gives 612. For a type that leaves the fleet within 10 years, shop visits after the window are almost entirely determined by retirement order. An aircraft with 3 or more years of on-wing life left never goes to the shop again.
We corrected the assumptions twice along the way. The empty years (2029 and 2030) were an artefact of “setting the state of the 83 undisclosed engines uniformly”; using the continuation of the 20-year simulation (first 5 years discarded) made the clusters disappear. We also changed the bar widths from a flat 4 months to the actual time each engine is off wing.
2. The operations side as the counterpart, and the assumption that new aircraft take over the flying
Every bar in the plan has a counterpart. Each month there is the “number of engines needed to fly” and the “spares on the shelf”, and the bars must keep “serviceable ≥ required + spares” against them. We drew this line on the sequence to retirement as well.
The transition assumption is “retire one 737-800 for each 737-8 delivered”, with the new aircraft taking over the flights. Delivery delays are where this breaks down: aircraft that were given only the lightest workscope to last until retirement keep flying, and need additional shop visits.
What we learned for Carrier A (synthetic data): with a 12-month delivery delay, if it comes as a surprise it costs +89 shop visits and $284M; if the workscopes are re-decided once the delay is known, +16 visits and $142M. The $142M difference is “the value of knowing the delivery outlook early”. The lightest workscope that lasts until retirement minimises cost, but is vulnerable to delivery delays.
3. One layer up: passenger demand
“Visualise passenger demand, how well each carrier is meeting it, and whether it can grow further.” This turned out not to be a separate feature but the top rung of the ladder. The upper layer sets what the lower layer “needs”; the lower layer returns “spare capacity” and “limits” to the layer above.
What we built: the demand layer (demand.py). The market comes from MLIT’s Air Transport Statistics (e-Stat); each carrier’s figures are estimated from published monthly data and press reports. It covers market share, a lower bound on “unmet demand” in months where the load factor exceeds a threshold, and engine spare capacity expressed as seats (spare capacity ÷ 2 = aircraft that can fly → ASK).
What we learned (real data): versus FY2019, the market shows demand +13%, supply −1%, and load factor 70.9% → 81.3%. The market has swung towards a seat shortage — a phase where spare aircraft and engine capacity turns into revenue. Carrier A (synthetic) had 4 tight months among the months with data, with a lower bound on unmet demand of 330 million RPK (≈ 430,000 passengers). Engine spare capacity inside the window is 15% of 737-800 supply. The gap between the seasonal shape of demand and the plan’s flight index is largest in October.
4. Turning past demand into planning assumptions
Once demand is known, the planning assumptions can be derived from it: the seasonal shape (monthly market RPK ÷ average), demand growth (annual rate of market RPK), and utilisation (the supply ratio needed to carry the growth while bringing the load factor back to the threshold). The past is not rewritten; only the assumptions from today’s engine state forward change (plan_from_demand.py).
The engine maintenance assumptions change as follows: utilisation ↑ → LLP and EGT limits arrive sooner and removal windows move earlier. Seasonal shape → peak periods and the number of engines needed. Growth → more positions to fill, and the pace of spares and retirements.
What we learned for Carrier A (synthetic), taking market growth of +3.8% per year as is: engines reaching their limit 40 → 41 (11 engines move 1 month earlier). Cancellation risk 0.2% → 17%. Engines required rise by +5 to +14 per month while the owned count stays the same. If the growth is taken at face value, the current fleet of engines is not enough.
As a by-product, we found a bug where LLP life from the previous engine type carried over globally when switching types, and rebuilt the Carrier B (synthetic) inputs (22 → 24 engines) and all baseline plans.
5. How often should growth be reviewed?
Compounding 3.8% over 10 years is dangerous. Re-estimate it once a year (the annual plan version) from the latest actuals. For the next 2 years use the recent trend; from year 5 onward converge to the long run (FY2018→25, spanning COVID, +1.5%). If any month’s market year-on-year change deviates from the assumption by 3 points or more, take it to the monthly meeting instead of waiting for the annual review.
What we learned (real data): market RPK in Jun 2026 was −3.1% year on year. The trigger has already been pulled. Growth over the last 2 years includes a post-COVID rebound. Read the 17% in section 4 as an upper-side indication.
6. Laying down a framework
We applied the same structure to every assumption, not just demand growth. The 16 assumptions are arranged in 7 layers (demand, fleet, operations, shops and supply, engines, money, decision rules), with source, review cycle, triggers for early review, what they affect and who owns them on a single sheet (data/assumptions_review.json). Expired assumptions and triggers are picked up automatically by the PDCA / OODA review (review.py).
Triggers currently pulled for Carrier A (synthetic): demand growth (−3.1% vs +3.8%), LLP kit lead time (change point detected), and approval line v (net additional work found at teardown +$6.9M).
7. Validating against the past
Whether the method is right can be checked by applying it to the past. Using the 20-year simulation as history, we built plans with the same method for the past 8 plan versions and compared them with the shop visits that actually happened afterwards (backtest.py). The demand-growth rules were tested on the real fiscal-year series, with each year forecast using only data up to the previous year.
What we learned for Carrier A (synthetic data):
- Years in which actuals fell inside the risk band: 78% (target 80%). The band is reasonable.
- Deadline forecasts are late by +1.5 months on average (actual removals come later than forecast). The plan is conservative. Move the start of the removal window later.
- Unscheduled removals ran at 1.67× the assumption. Raise the rate (the same conclusion as the learning carried into the next version).
- Plan-version stability 28%: the share of the previous version’s year 2 that remains unchanged as the next version’s year 1.
What we learned (demand, real data): in 3 of the 5 validation years, shocks (beyond ±15%) made every rule miss. In the 2 normal years the long-run rule had an error of 2.8%, the recent-trend rule 23.9%. The current blend, which uses the recent trend for the first 12 months of the plan, does worse than the long-run rule on real data. This is the basis for shifting the default towards the long run.
7b. Learn, verify, use
Scoring corrections found in validation on the same data would be circular. So we split the 20 years into four parts: burn-in (years 0–5, discarded), learn (9 versions in years 6–14: derive corrections), verify (5 versions in years 15–19: score with and without corrections), and use (the plan from today: carry forward only the corrections that improved results).
What we learned for Carrier A (synthetic data): the learned corrections were “deadlines +2 months, unscheduled rate ×1.54, share of planned shop visits that happen in their year 0.81”. In the verify period, the deadline error shrank from +1.5 to −0.5 months (can be carried forward), years inside the band stayed at 93%, and the visit-count forecast worsened from 0.95 to 0.89 (not carried forward). 1 of 3 metrics improved. Only the deadline shift is carried into the use period. Carrier B (synthetic) shows the same pattern (deadline +1.4 → −0.6; within ±1 month 53% → 60%). Metrics where corrections help and those where they do not separated clearly.
7c. The engine-type clock
Above each engine’s own clock sits the generational clock of the engine type. The CFM56-7B entered service in 1997, 737NG production ended in Jan 2020, its successor the LEAP-1B entered service in 2017, and the generation after that, RISE, is targeted for 2035. Generations are about 20 years apart, and an airframe lasts about 25 years. So the 737-800 will leave the world fleet around 2045.
What we built: the engine-type clock (typelife.py). A residual-value multiplier by retirement year (1.0 at end of production, 0.35 in the year the type disappears; assumed), late-life parts price increases and longer turnaround times, and the probability of delivery delays based on the age of the next generation.
What we learned for Carrier A (synthetic): the CFM56-7B is in its 30th year of service and 7 years past end of production — late life. The residual value realised at retirement drops from $75M to $51M (a $24M loss from selling late in the type’s life). Our final retirement in 2036 is 9 years before the type disappears, so buyers still exist. Decide how to dispose of retiring aircraft before the type enters late life.
7d. Is the shop the bottleneck?
“Is the slowness a sign that we need to develop new shops?” The numbers said no. Of the 12-month loop time, 8 months is LLP kit lead time (on the supplier side). Of 24 tracking exceptions, 2 were late returns and 17 were cost overruns. Shop induction slots are at 9 of 10 — no slack, but not the binding constraint — and building a new domestic shop has a negative NPV under every decision rule.
So the order is “LLP kits → contract → option on a second shop”. We layered these onto the same simulation in turn and solved under three assumption sets (baseline, tight, late life) (playbook.py). For Carrier A (synthetic): kits cost −$0.1M but shorten the loop from 12 to 7 months (a speed lever). Splitting findings costs 50/50 in the contract has an expected value of −$4.7M, with a recovery cost of −$15.5M. An option on a second shop costs −$1.5M but raises cancellation risk by +1.9 pt. Secure shop capacity through volume commitments and options, not by developing new shops. The option needs a condition: “use it only in months when induction slots are full”.
8. Conclusion: three decision rules to fix, and one continuous line
This work narrowed the decision rules to fix in the next plan version down to three.
- Shift the default for demand growth towards the long run (the recent trend is downgraded to an input for triggers). The long-run rule won in validation on real data.
- Move shop-visit deadlines 1–2 months later (forecasts are conservative). This also held outside the learn and verify periods.
- The unscheduled removal rate is a candidate for upward revision, but in the verify period it made the visit-count forecast worse. In the next version, lengthen the learn period and re-assess.
And the structure became clear: one engine’s shop visit → the 2-year plan → the sequence to retirement → fleet transition → routes and passenger demand → company strategy. The upper layer sets what the lower layer “needs”; the lower layer returns “spare capacity” and “limits” to the layer above. OODA running inside PDCA has the same shape. Management is buying the future. This system connects that purchase in one continuous line down to a single engine’s shop visit, and shows which part of the line has gone stale.
Appendix: where to check
| Section | Report view | Output |
|---|---|---|
| 1, 2 | Shop-visit sequence to retirement (annual notes, operations side, delivery delays) | runout/*.json |
| 3 | Passenger demand (one layer up) | demand/*.json, data/demand.json |
| 4, 5 | Passenger demand → “Turning past demand into planning assumptions”, “Reviewing growth” | demand/*-plan.json |
| 6 | PDCA / OODA review → “Framework for reviewing assumptions” | data/assumptions_review.json, review/*.json |
| 7 | Validation against the past (backtest) | backtest/*.json |