Saving Jet Fuel Airbus's open-source Scikit-decide framework, developed over six years, can optimize flight paths to cut jet fuel consumption, potentially saving a few thousand dollars on a Boeing 787-9 Newark-to-Rome flight that would otherwise burn roughly $68,000 in fuel over 8.5 hours. The framework pairs with the OpenAP aircraft performance model built by Dr. Junzi Sun, a tenured assistant professor at Delft University of Technology, and NOAA wind data to solve optimal routes, and can also reorganize airline workforce schedules and calculate drone swarm paths. The write-up compares two flight paths flown using the Airbus A320 and OpenAP's fuel consumption model, with Scikit-decide version 1.1.1 installed alongside OpenAP and OpenTop 2.6.0. A Boeing 787-9 Dreamliner flying nonstop from Newark Liberty International Airport EWR to Leonardo da Vinci-Fiumicino Airport FCO could need $68K in jet fuel over the 8.5-hour flight. Adjusting the flight path for wind conditions could reduce fuel consumption and possibly save a few thousand dollars. Firms like Jeppesen have offerings in this space, but Scikit-decide https://airbus.github.io/scikit-decide , together with a narrow- and wide-body fuel consumption model built by a professor at the Delft University of Technology and wind data from NOAA, offer an open source solution. Scikit-decide has been in development for six years. It's a framework for reinforcement learning, automated planning and scheduling. The project can optimise flight paths, re-organise airline workforce schedules and calculate drone swarm paths. OpenAP https://openap.dev/ is an aircraft performance model and toolkit developed by Dr. Junzi Sun. Dr. Sun has a PhD in air traffic management and, among many other things, teaches a course on the subject as a tenured assistant professor at TU Delft in the Netherlands. Scikit-decide's optimal flight path solver can be configured to use different fuel consumption models. In this post, I'll compare two flight paths flown using the Airbus A320 and OpenAP's fuel consumption model. My Workstation I'm using a 5.7 GHz AMD Ryzen 9 9950X CPU. It has 16 cores and 32 threads and 1.2 MB of L1, 16 MB of L2 and 64 MB of L3 cache. It has a liquid cooler attached and is housed in a spacious, full-sized Cooler Master HAF 700 computer case. The system has 96 GB of DDR5 RAM clocked at 4,800 MT/s and a 5th-generation, Crucial T700 4 TB NVMe M.2 SSD which can read at speeds up to 12,400 MB/s. There is a heatsink on the SSD to help keep its temperature down. This is my system's C drive. The system is powered by a 1,200-watt, fully modular Corsair Power Supply and is sat on an ASRock X870E Nova 90 Motherboard. I'm running Ubuntu 24 LTS via Microsoft's Ubuntu for Windows on Windows 11 Pro. In case you're wondering why I don't run a Linux-based desktop as my primary work environment, I'm still using an Nvidia GTX 1080 GPU which has better driver support on Windows and ArcGIS Pro only supports Windows natively. Installing Prerequisites I'll use Python 3.12 along with jq in this post. bash $ sudo add-apt-repository ppa:deadsnakes/ppa $ sudo apt update $ sudo apt install \ jq \ python3-pip \ python3.12-venv I'll set up a Python Virtual Environment and install scikit-decide, along with the OpenAP open aircraft performance model and OpenTop, a flight trajectory toolkit that was also developed by Dr. Sun. bash $ python3 -m venv ~/.flight planning $ source ~/.flight planning/bin/activate $ pip install \ 'scikit-decide all ' \ 'openap all ' \ opentop The above will need at least 8 GB of storage capacity. These are the packages that were installed. bash $ pip install pipdeptree $ pipdeptree -d0 lz4==4.4.5 openevolve==0.3.2 opentop==2.6.0 pip==24.0 pipdeptree==4.2.5 plado==0.1.6 pygeodesy==26.9.9 pygrib==2.1.8 pyRDDLGym-gurobi==0.2 pyRDDLGym-jax==3.1 pyRDDLGym-rl==0.2 pytz==2026.3.post1 ray==2.37.0 rddlrepository==2.2 sb3 contrib==2.3.0 scikit-decide==1.1.1 scikit-image==0.26.0 tensorboardX==2.6.5 torch-geometric==2.8.0.post1 typer==0.27.2 unified-planning==1.2.0 up-enhsp==0.0.27 up fast downward==0.5.2 up-pyperplan==1.1.0 z3-solver==5.1.0.0 I'll use DuckDB, along with its H3 https://github.com/isaacbrodsky/h3-duckdb , JSON https://duckdb.org/docs/extensions/json , Lindel https://community-extensions.duckdb.org/extensions/lindel.html , Parquet https://duckdb.org/docs/data/parquet/overview and Spatial https://duckdb.org/docs/extensions/spatial.html extensions in this post. bash $ cd ~ $ wget -c https://github.com/duckdb/duckdb/releases/download/v1.5.4/duckdb cli-linux-amd64.zip $ unzip -j duckdb cli-linux-amd64.zip $ chmod +x duckdb $ ~/duckdb INSTALL h3 FROM community; INSTALL lindel FROM community; INSTALL json; INSTALL parquet; INSTALL spatial; I'll set up DuckDB to load every installed extension each time it launches. bash $ vi ~/.duckdbrc .timer on .width 180 LOAD h3; LOAD lindel; LOAD json; LOAD parquet; LOAD spatial; The maps in this post were rendered with QGIS https://www.qgis.org/en/site/forusers/download.html version 4.2.1. QGIS is a desktop application that runs on Windows, macOS and Linux. The application has grown in popularity in recent years and has ~22M application launches from users all around the world each month. The boundaries and place names were sourced from Natural Earth natural-earth-free-gis-data.html . Maritime Boundaries were sourced from Marine Regions https://marineregions.org/downloads.php . OpenAP's Aircraft Types I'll first clone the OpenAP repository. bash $ git clone https://github.com/junzis/openap Excluding unit tests and utility scripts, there are 3,369 lines of Python in this package. OpenAP's model relies on a large number of datasets that are packaged with its codebase. These cover a wide variety of aircraft. Below are the aircraft manufacturer counts. bash $ grep -ho 'aircraft: . a-z ' \ openap/data/aircraft/ .yml \ | cut -d' ' -f2 \ | sort \ | uniq -c \ | sort -rn 17 Boeing 13 Airbus 5 Embraer 1 Gulfstream 1 Cessna These are the properties for the Airbus A380-800. bash $ cat openap/data/aircraft/a388.yml aircraft: Airbus A380-800 mtow: 560000 mlw: 386000 oew: 277000 mfc: 320000 vmo: 340 mmo: 0.89 ceiling: 13100 pax: max: 853 low: 410 high: 620 fuselage: length: 72.72 height: 8.41 width: 7.14 wing: area: 845 span: 79.75 mac: null sweep: 33.5 t/c: 0.08 flaps: type: single-slotted area: null bf/b: null lambda f: 0.900 cf/c: 0.150 Sf/S: 0.150 cruise: height: 12800 mach: 0.85 range: 14800 engine: type: turbofan mount: wing number: 4 default: GP7270 options: A380-841: Trent 970-84 A380-842: Trent 972-84 A380-861: GP7270 drag: cd0: 0.016 k: 0.050 e: 0.855 gears: 0.012 These are its drag coefficients. bash $ cat openap/data/dragpolar/a388.yml aircraft: Airbus A380-800 clean: cd0: 0.016 k: 0.050 e: 0.855 gears: 0.012 flaps: lambda f: 0.900 cf/c: 0.150 Sf/S: 0.150 These are some additional properties. python $ echo "import pandas as pd; print pd.read fwf 'openap/data/wrap/a388.txt' .to csv index=False " \ | python3 \ | ~/duckdb \ -c '.maxwidth 150' \ -c "SELECT EXCLUDE parameters , parameters: SPLIT parameters, '|' FROM READ CSV '/dev/stdin' " ┌──────────────────────┬────────────────┬───────────────────────────────────────┬────────┬────────┬─────────┬─────────┬──────────────────────────────┐ │ variable │ flight phase │ name │ opt │ min │ max │ model │ parameters │ │ varchar │ varchar │ varchar │ double │ double │ double │ varchar │ varchar │ ├──────────────────────┼────────────────┼───────────────────────────────────────┼────────┼────────┼─────────┼─────────┼──────────────────────────────┤ │ to v lof │ takeoff │ Liftoff speed │ 89.9 │ 75.4 │ 104.4 │ norm │ 89.93, 10.07 │ │ to d tof │ takeoff │ Takeoff distance │ 2.56 │ 1.35 │ 3.78 │ norm │ 2.56, 0.74 │ │ to acc tof │ takeoff │ Mean takeoff accelaration │ 1.35 │ 1.04 │ 1.66 │ norm │ 1.35, 0.19 │ │ ic va avg │ initial climb │ Mean airspeed │ 88.0 │ 80.0 │ 96.0 │ norm │ 88.15, 5.64 │ │ ic vs avg │ initial climb │ Mean vertical rate │ 5.65 │ 4.4 │ 8.94 │ gamma │ 4.76, 3.22, 0.65 │ │ cl d range │ climb │ Climb range │ 296.0 │ 200.0 │ 446.0 │ beta │ 3.23, 5.18, 179.46, 335.24 │ │ cl v cas const │ climb │ Constant CAS │ 163.0 │ 155.0 │ 170.0 │ norm │ 163.39, 4.51 │ │ cl v mach const │ climb │ Constant Mach │ 0.84 │ 0.8 │ 0.86 │ beta │ 12.23, 5.32, 0.72, 0.17 │ │ cl h cas const │ climb │ Constant CAS crossover altitude │ 3.3 │ 1.3 │ 5.3 │ norm │ 3.29, 1.24 │ │ cl h mach const │ climb │ Constant Mach crossover altitude │ 8.9 │ 8.2 │ 9.7 │ norm │ 8.94, 0.47 │ │ cl vs avg pre cas │ climb │ Mean climb rate, pre-constant-CAS │ 7.85 │ 5.95 │ 9.75 │ norm │ 7.85, 1.16 │ │ cl vs avg cas const │ climb │ Mean climb rate, constant-CAS │ 7.51 │ 5.2 │ 9.82 │ norm │ 7.51, 1.40 │ │ cl vs avg mach const │ climb │ Mean climb rate, constant-Mach │ 5.56 │ 3.23 │ 7.91 │ norm │ 5.57, 1.42 │ │ cr d range │ cruise │ Cruise range │ 4348.0 │ 892.0 │ 20565.0 │ gamma │ 2.81, 246.73, 2274.81 │ │ cr v cas mean │ cruise │ Mean cruise CAS │ 136.0 │ 130.0 │ 145.0 │ beta │ 3.32, 5.27, 126.00, 29.75 │ │ cr v cas max │ cruise │ Maximum cruise CAS │ 145.0 │ 134.0 │ 164.0 │ beta │ 2.02, 3.21, 130.38, 46.65 │ │ cr v mach mean │ cruise │ Mean cruise Mach │ 0.84 │ 0.82 │ 0.86 │ norm │ 0.84, 0.01 │ │ cr v mach max │ cruise │ Maximum cruise Mach │ 0.87 │ 0.85 │ 0.9 │ gamma │ 16.14, 0.80, 0.00 │ │ cr h init │ cruise │ Initial cruise altitude │ 11.55 │ 9.3 │ 12.23 │ beta │ 3.82, 1.66, 7.49, 5.01 │ │ cr h mean │ cruise │ Mean cruise altitude │ 11.73 │ 10.87 │ 12.28 │ beta │ 7.22, 3.92, 9.59, 3.14 │ │ cr h max │ cruise │ Maximum cruise altitude │ 12.06 │ 11.52 │ 12.6 │ norm │ 12.06, 0.33 │ │ de d range │ descent │ Descent range │ 310.0 │ 238.0 │ 528.0 │ gamma │ 4.73, 213.47, 25.87 │ │ de v mach const │ descent │ Constant Mach │ 0.83 │ 0.8 │ 0.87 │ norm │ 0.83, 0.02 │ │ de v cas const │ descent │ Constant CAS │ 154.0 │ 142.0 │ 167.0 │ norm │ 154.84, 7.74 │ │ de h mach const │ descent │ Constant Mach crossover altitude │ 10.1 │ 8.6 │ 11.5 │ norm │ 10.06, 0.88 │ │ de h cas const │ descent │ Constant CAS crossover altitude │ 6.6 │ 3.9 │ 9.4 │ norm │ 6.64, 1.69 │ │ de vs avg mach const │ descent │ Mean descent rate, constant-Mach │ -6.06 │ -11.9 │ -2.97 │ beta │ 3.43, 2.08, -15.98, 14.36 │ │ de vs avg cas const │ descent │ Mean descent rate, constant-CAS │ -8.36 │ -11.74 │ -4.97 │ norm │ -8.36, 2.06 │ │ de vs avg after cas │ descent │ Mean descent rate, after-constant-CAS │ -5.48 │ -6.93 │ -4.02 │ norm │ -5.48, 0.88 │ │ fa va avg │ final approach │ Mean airspeed │ 73.0 │ 68.0 │ 77.0 │ norm │ 73.28, 3.02 │ │ fa vs avg │ final approach │ Mean vertical rate │ -3.71 │ -4.13 │ -2.92 │ gamma │ 9.49, -4.74, 0.12 │ │ fa agl │ final approach │ Approach angle │ 2.9 │ 2.42 │ 3.38 │ norm │ 2.90, 0.29 │ │ ld v app │ landing │ Touchdown speed │ 70.0 │ 62.1 │ 78.0 │ norm │ 70.00, 5.52 │ │ ld d brk │ landing │ Braking distance │ 2.26 │ 0.73 │ 3.8 │ norm │ 2.26, 0.93 │ │ ld acc brk │ landing │ Mean braking acceleration │ -1.01 │ -1.51 │ -0.52 │ norm │ -1.01, 0.30 │ └──────────────────────┴────────────────┴───────────────────────────────────────┴────────┴────────┴─────────┴─────────┴──────────────────────────────┘ These are the aircraft type synonyms list. $ ~/duckdb -c "FROM READ CSV '/dev/stdin' " \ < openap/data/aircraft/ synonym.csv ┌─────────┬─────────┐ │ orig │ new │ │ varchar │ varchar │ ├─────────┼─────────┤ │ a124 │ b744 │ │ a306 │ a332 │ │ a310 │ a318 │ │ at72 │ e145 │ │ at75 │ e145 │ │ at76 │ e145 │ │ b733 │ b734 │ │ b735 │ b734 │ │ b762 │ b763 │ │ b77l │ b77w │ │ c25a │ c550 │ │ c525 │ c550 │ │ c56x │ c550 │ │ crj2 │ e145 │ │ crj9 │ e75l │ │ e290 │ e190 │ │ glf5 │ glf6 │ │ gl5t │ glf6 │ │ lj45 │ glf6 │ │ md11 │ b773 │ │ pc24 │ c550 │ │ su95 │ e170 │ └─────────┴─────────┘ Aircraft Engines Aircraft often have the option of at least two different engines to choose from. There are 427 engines listed in this package's dataset. bash $ wc -l openap/data/engine/engines.csv 427 These are the details for the Trent 970-84. bash $ echo "FROM 'openap/data/engine/engines.csv' WHERE name = 'Trent 970-84' LIMIT 1" \ | ~/duckdb -json \ | jq -S . { "bpr": 8.45, "cruise alt": null, "cruise mach": null, "cruise sfc": null, "cruise thrust": null, "ei co app": 1.16, "ei co co": 0.31, "ei co idl": 13.38, "ei co to": 0.32, "ei hc app": 0.08, "ei hc co": 0.12, "ei hc idl": 0.04, "ei hc to": 0.02, "ei nox app": 12.09, "ei nox co": 29.42, "ei nox idl": 5.44, "ei nox to": 38.29, "ff app": 0.72, "ff co": 2.157, "ff idl": 0.255, "ff to": 2.605, "fuel lto": 965.0, "manufacturer": "Rolls-Royce plc", "max thrust": 338700.0, "name": "Trent 970-84", "pr": 38.0, "type": "TF", "uid": "18RR081" } These are the engine manufacturer counts. $ ~/duckdb CREATE OR REPLACE TABLE a AS FROM 'openap/data/engine/engines.csv'; SELECT COUNT , manufacturer FROM a GROUP BY 2 ORDER BY 1 DESC; ┌──────────────┬────────────────────────────┐ │ count star │ manufacturer │ │ int64 │ varchar │ ├──────────────┼────────────────────────────┤ │ 108 │ GE Aircraft Engines │ │ 94 │ CFM International │ │ 85 │ Pratt & Whitney │ │ 62 │ Rolls-Royce plc │ │ 13 │ International Aero Engines │ │ 12 │ Pratt & Whitney Canada │ │ 11 │ Rolls-Royce Corporation │ │ 8 │ Rolls-Royce Deutschland │ │ 8 │ Honeywell │ │ 7 │ Aviadvigatel │ │ 5 │ Textron Lycoming │ │ 4 │ KKBM │ │ 3 │ IVCHENKO PROGRESS ZMBK │ │ 2 │ PowerJet S.A. │ │ 2 │ Allied Signal │ │ 1 │ Engine Alliance │ │ 1 │ Garret AiResearch │ └──────────────┴────────────────────────────┘ These are the engine-type counts for Turbofan TF , Mixed-flow Turbofan MTF , Turboprop TP and Piston PS engines in this dataset. SELECT COUNT , type FROM a GROUP BY 2 ORDER BY 1 DESC; ┌──────────────┬─────────┐ │ count star │ type │ │ int64 │ varchar │ ├──────────────┼─────────┤ │ 322 │ TF │ │ 98 │ MTF │ │ 5 │ TP │ │ 1 │ PS │ └──────────────┴─────────┘ This is the engine list ranked by their maximum thrust. SELECT manufacturer, name, type, max thrust FROM a ORDER BY 4 DESC LIMIT 25; ┌─────────────────────┬───────────────┬─────────┬────────────┐ │ manufacturer │ name │ type │ max thrust │ │ varchar │ varchar │ varchar │ double │ ├─────────────────────┼───────────────┼─────────┼────────────┤ │ GE Aircraft Engines │ GE90-115B │ TF │ 513900.0 │ │ GE Aircraft Engines │ GE90-113B │ TF │ 504900.0 │ │ GE Aircraft Engines │ GE90-110B1 │ TF │ 492600.0 │ │ Rolls-Royce plc │ Trent XWB-97 │ TF │ 436748.0 │ │ GE Aircraft Engines │ GE90-94B │ TF │ 430920.0 │ │ GE Aircraft Engines │ GE90-92B │ TF │ 426720.0 │ │ GE Aircraft Engines │ GE90-90B │ TF │ 419250.0 │ │ Rolls-Royce plc │ Trent 895 │ TF │ 413050.0 │ │ Rolls-Royce plc │ Trent 892 │ TF │ 411480.0 │ │ Pratt & Whitney │ PW4090 │ TF │ 408300.0 │ │ GE Aircraft Engines │ GE90-85B │ TF │ 397210.0 │ │ Rolls-Royce plc │ Trent 884 │ TF │ 390100.0 │ │ Pratt & Whitney │ PW4084D │ TF │ 385900.0 │ │ Rolls-Royce plc │ Trent XWB-84 │ TF │ 379000.0 │ │ Pratt & Whitney │ PW4084 │ TF │ 369600.0 │ │ GE Aircraft Engines │ GE90-77B │ TF │ 366750.0 │ │ Rolls-Royce plc │ Trent 1000-R3 │ TF │ 363900.0 │ │ GE Aircraft Engines │ GE90-76B │ TF │ 363220.0 │ │ Rolls-Royce plc │ Trent 877 │ TF │ 361640.0 │ │ Rolls-Royce plc │ Trent 1000-M3 │ TF │ 358100.0 │ │ Rolls-Royce plc │ Trent 1000-N3 │ TF │ 358100.0 │ │ Pratt & Whitney │ PW4077D │ TF │ 355700.0 │ │ Rolls-Royce plc │ Trent XWB-79B │ TF │ 355200.0 │ │ Rolls-Royce plc │ Trent XWB-79 │ TF │ 355200.0 │ │ Rolls-Royce plc │ Trent 970B-84 │ TF │ 352900.0 │ └─────────────────────┴───────────────┴─────────┴────────────┘ These are the fuel model defaults and overrides. $ ~/duckdb -c "FROM READ CSV '/dev/stdin' " \ < openap/data/fuel/fuel models.csv ┌──────────┬─────────────┬────────────────────┬────────────────────┬────────────────────┐ │ typecode │ engine type │ c1 │ c2 │ c3 │ │ varchar │ varchar │ double │ double │ double │ ├──────────┼─────────────┼────────────────────┼────────────────────┼────────────────────┤ │ A318 │ CFM56-5B9/3 │ 0.7769784596099123 │ 1.765377288174942 │ 2.5349134936316693 │ │ A319 │ V2524-A5 │ 0.8694169413032631 │ 1.9542690629047836 │ 2.5028187026860103 │ │ A320 │ CFM56-5B4/P │ 1.0453208160586924 │ 2.3633720747416573 │ 1.2378127479131922 │ │ A321 │ V2533-A5 │ 1.3979999999999444 │ 2.054028451829268 │ 1.0008941993511127 │ │ A332 │ Trent 772 │ 2.886430057340283 │ 1.0960397632560752 │ 2.3772585567580293 │ │ A333 │ Trent 772 │ 3.1199999999999997 │ 1.0365152289922772 │ 1.950599421257047 │ │ B737 │ CFM56-7B26 │ 1.0237419750954273 │ 1.4670109921175798 │ 3.2566140275646456 │ │ B738 │ CFM56-7B26E │ 1.075484518912494 │ 1.8777303165419037 │ 1.8895522140156369 │ │ B739 │ CFM56-7B27E │ 1.3079999999999998 │ 1.5986016771932572 │ 1.2789091908108752 │ │ CRJ9 │ CF34-8C5 │ 0.6437136288905128 │ 1.9690234662778772 │ 1.4375859706162741 │ │ E170 │ CF34-8E5 │ 0.6341784688704629 │ 2.778729428440142 │ 1.0149695061665696 │ │ E190 │ CF34-10E5 │ 0.8339999999998783 │ 2.3343013671118475 │ 0.4847704716061958 │ │ E195 │ CF34-10E5A1 │ 0.911999999999993 │ 1.929664699695295 │ 0.8452746256489131 │ │ E75L │ CF34-8E5 │ 0.6340709359225759 │ 2.614653287356019 │ 0.8714282723568036 │ │ default │ default │ 0.937564901246902 │ 1.9767611682280135 │ 1.3954794843472482 │ └──────────┴─────────────┴────────────────────┴────────────────────┴────────────────────┘ Toulouse to Berlin Below, I'll find an optimal flight path from Toulouse-Blagnac Airport LFBO / TLS to Berlin Brandenburg Airport EDDB / BER . bash $ python3 python import numpy as np from openap.aero import cas2mach, ft, kts from openap.extra.nav import airport from pygeodesy.ellipsoidalVincenty import LatLon from skdecide.hub.domain\ .flight planning\ .aircraft performance\ .bean.aircraft state \ import AircraftState from skdecide.hub.domain\ .flight planning\ .aircraft performance\ .performance.performance model enum \ import PerformanceModelEnum from skdecide.hub.domain\ .flight planning\ .aircraft performance\ .performance.phase enum \ import PhaseEnum from skdecide.hub.domain\ .flight planning\ .aircraft performance\ .performance.rating enum \ import RatingEnum from skdecide.hub.domain\ .flight planning\ .domain \ import FlightPlanningDomain, \ WeatherDate from skdecide.hub.domain\ .flight planning\ .flightplanning utils \ import plot network adapted from skdecide.hub.solver.astar import Astar The heuristic parameter can be either "time", "distance", "lazy fuel", "lazy time", or None. If nothing is passed, A will use a Dijkstra-like search algorithm. origin = "LFPG" destination = "LFBO" aircraft = "A320" weather date = WeatherDate day=1, month=5, year=2026 heuristic = "lazy fuel" cost function = "fuel" acState = AircraftState model type="A320", performance model type=PerformanceModelEnum.OPENAP, gw kg=80 000, zp ft=10 000, mach=cas2mach 250 kts, h=10 000 ft , phase=PhaseEnum.CLIMB, rating level=RatingEnum.MCL, cg=0.3 domain factory = lambda: FlightPlanningDomain aircraft state=acState, mach cruise=0.78, mach climb=0.7, mach descent=0.65, nb forward points=20, nb lateral points=10, nb climb descent steps=5, flight levels ft=list np.arange 30 000, 38 000 + 2 000, 2 000 , graph width="medium", origin=LatLon 43.629444, 1.363056 , destination="EDDB", objective=cost function, heuristic name=heuristic, weather date=weather date domain = domain factory When the above runs, if weather data hasn't been fetched from NOAA and if the date of the flight is within the past six months, GRB2 files will be downloaded. bash $ du -hs ~/skdecide data/weather/grib/nowcast/ / .grb2 144M /home/mark/skdecide data/weather/grib/nowcast/20260501/gfs 4 20260501 0000 000.grb2 144M /home/mark/skdecide data/weather/grib/nowcast/20260501/gfs 4 20260501 0600 000.grb2 143M /home/mark/skdecide data/weather/grib/nowcast/20260501/gfs 4 20260501 1200 000.grb2 143M /home/mark/skdecide data/weather/grib/nowcast/20260501/gfs 4 20260501 1800 000.grb2 Each file has data covering the entire planet. These are the contents of gfs 4 20260501 1800 000.grb2 rendered on a globe in QGIS. This is the solver's altitude and geographical search space. plot network adapted graph=domain.network, p0=LatLon 43.629444, 1.363056 , p1=LatLon airport "EDDB" "lat" , airport "EDDB" "lon" , airport "EDDB" "alt" ft This is the optimal flight path according to the solver. solver = Astar domain factory=domain factory, heuristic=lambda d, s: d.heuristic s , parallel=False solver.solve A finished to solve from state ... in 0.28 seconds domain.custom rollout solver=solver, make img=True Goal reached after 19 steps {'time': 7666.281474928903, 'fuel': 5855.093906205222}, None I'll format each of the flight plan's steps so they're easier to read. domain.observation.trajectory.to csv 'TLS-BER.csv', index=None $ ~/duckdb SELECT phase: UPPER phase , time : ts::INT, alt: alt::INT, mass: mass::INT, mach: ROUND mach, 2 , cas: cas::INT, fuel: fuel::INT, geom: ST POINT lon, lat FROM 'TLS-BER.csv' ORDER BY ts; ┌─────────┬───────┬───────┬───────┬────────┬───────┬───────┬────────────────────────────────────────────────┐ │ phase │ time │ alt │ mass │ mach │ cas │ fuel │ geom │ │ varchar │ int32 │ int32 │ int32 │ double │ int32 │ int32 │ geometry │ ├─────────┼───────┼───────┼───────┼────────┼───────┼───────┼────────────────────────────────────────────────┤ │ CLIMB │ 28800 │ 0 │ 80000 │ 0.45 │ 154 │ 0 │ POINT 1.363056 43.629444 │ │ CLIMB │ 29183 │ 12000 │ 79402 │ 0.7 │ 194 │ 598 │ POINT 1.3614644301412264 44.431571122861556 │ │ CLIMB │ 29767 │ 18000 │ 78717 │ 0.7 │ 173 │ 685 │ POINT 0.8028803270337778 45.54329961845038 │ │ CLIMB │ 30367 │ 24000 │ 78104 │ 0.7 │ 154 │ 2 │ POINT 0.22308186850312028 46.64860136322122 │ │ CLIMB │ 30369 │ 24000 │ 78102 │ 0.7 │ 154 │ 2 │ POINT 0.2213600223765711 46.65181397470445 │ │ CLIMB │ 30691 │ 30000 │ 77808 │ 0.7 │ 135 │ 294 │ POINT 0.781910701674247 47.146798229437145 │ │ CRUISE │ 31222 │ 30000 │ 77331 │ 0.78 │ 152 │ 477 │ POINT 0.18057649919536045 48.254448220756515 │ │ CRUISE │ 31515 │ 30000 │ 77070 │ 0.78 │ 152 │ 262 │ POINT 0.7571242215277763 48.74964861278412 │ │ CRUISE │ 31805 │ 30000 │ 76812 │ 0.78 │ 152 │ 258 │ POINT 1.3446668642885302 49.24219441171162 │ │ CRUISE │ 32093 │ 30000 │ 76555 │ 0.78 │ 152 │ 256 │ POINT 1.943586016571517 49.732000774167815 │ │ CRUISE │ 32382 │ 30000 │ 76298 │ 0.78 │ 152 │ 257 │ POINT 2.554285524033502 50.218985517421046 │ │ CRUISE │ 32672 │ 30000 │ 76041 │ 0.78 │ 152 │ 257 │ POINT 3.1771982015706532 50.70307279893911 │ │ CRUISE │ 32961 │ 30000 │ 75786 │ 0.78 │ 152 │ 256 │ POINT 3.812797488711077 51.18419969012497 │ │ CRUISE │ 33252 │ 30000 │ 75529 │ 0.78 │ 152 │ 257 │ POINT 4.461619033936127 51.66232853895605 │ │ CRUISE │ 33547 │ 32000 │ 75270 │ 0.78 │ 146 │ 259 │ POINT 5.1243038676232 52.13747186609129 │ │ CRUISE │ 34103 │ 30000 │ 74794 │ 0.78 │ 152 │ 476 │ POINT 7.006628090474678 51.93831668841654 │ │ DESCENT │ 34458 │ 24031 │ 74503 │ 0.65 │ 142 │ 290 │ POINT 7.7011825929472675 52.40044729715865 │ │ DESCENT │ 35058 │ 18063 │ 73995 │ 0.65 │ 160 │ 51 │ POINT 9.423199233939213 52.1821111144618 │ │ DESCENT │ 35114 │ 18063 │ 73944 │ 0.65 │ 160 │ 51 │ POINT 9.580613666598857 52.160789987133924 │ │ DESCENT │ 35714 │ 12094 │ 73388 │ 0.65 │ 179 │ 13 │ POINT 11.398384342445423 51.895274115132494 │ │ DESCENT │ 35727 │ 12094 │ 73375 │ 0.65 │ 179 │ 13 │ POINT 11.435080901553494 51.88959204464927 │ │ DESCENT │ 36063 │ 6126 │ 73023 │ 0.65 │ 199 │ 352 │ POINT 12.17769156482302 52.32776873134525 │ │ DESCENT │ 36466 │ 48 │ 72534 │ 0.65 │ 221 │ 488 │ POINT 13.48503 52.36769 │ └─────────┴───────┴───────┴───────┴────────┴───────┴───────┴────────────────────────────────────────────────┘ I'll export the flight plan to Parquet and render it on top of the ground-level wind data in QGIS. COPY SELECT EXCLUDE lon, lat , geometry: ST POINT lon, lat FROM 'TLS-BER.csv' ORDER BY ts TO 'TLS-BER.parquet' FORMAT 'PARQUET', CODEC 'ZSTD', COMPRESSION LEVEL 22, ROW GROUP SIZE 15000 ; Toulouse to Warsaw Below, I'll find an optimal flight path from Toulouse-Blagnac Airport LFBO / TLS to Warsaw Chopin Airport EPWA / WAW . The initial target altitude will be much higher than in the previous example. The result is a flight that is able to take a much more direct route. acState = AircraftState model type="A320", performance model type=PerformanceModelEnum.OPENAP, gw kg=80 000, zp ft=18000.0, mach=cas2mach 250 kts, h=10 000 ft , phase=PhaseEnum.CLIMB, rating level=RatingEnum.MCL, cg=0.3, x graph=5, y graph=5, z graph=10 domain factory = lambda: FlightPlanningDomain aircraft state=acState, mach cruise=0.78, mach climb=0.7, mach descent=0.65, nb forward points=20, nb lateral points=10, nb climb descent steps=5, flight levels ft=list np.arange 30 000, 38 000 + 2 000, 2 000 , graph width="medium", origin=LatLon 43.629444, 1.363056 , destination="EPWA", objective=cost function, heuristic name=heuristic, weather date=weather date domain = domain factory solver = Astar domain factory=domain factory, heuristic=lambda d, s: d.heuristic s , parallel=False solver.solve A finished to solve from state ... in 29.45 seconds. domain.custom rollout solver=solver, make img=True Goal reached after 14 steps {'time': 6153.660431613251, 'fuel': 5600.171145693044}, None Warsaw is 500 KM further away from Toulouse than Berlin. But the faster climb to cruising altitude under the given wind conditions meant the aircraft could take a more direct route. It made it to Warsaw almost 45 minutes faster and only needed 76% of the fuel that the Berlin flight needed. These are the steps in the above flight plan. domain.observation.trajectory.to csv 'TLS-WAW.csv', index=None $ ~/duckdb SELECT phase: UPPER phase , time : ts::INT, alt: alt::INT, mass: mass::INT, mach: ROUND mach, 2 , cas: cas::INT, fuel: fuel::INT, geom: ST POINT lon, lat FROM 'TLS-WAW.csv' ORDER BY ts; ┌─────────┬───────┬───────┬───────┬────────┬───────┬───────┬───────────────────────────────────────────────┐ │ phase │ time │ alt │ mass │ mach │ cas │ fuel │ geom │ │ varchar │ int32 │ int32 │ int32 │ double │ int32 │ int32 │ geometry │ ├─────────┼───────┼───────┼───────┼────────┼───────┼───────┼───────────────────────────────────────────────┤ │ CLIMB │ 28800 │ 30000 │ 80000 │ 0.45 │ 85 │ 0 │ POINT 6.321780309765473 45.78957852956533 │ │ CRUISE │ 29196 │ 32000 │ 79639 │ 0.78 │ 146 │ 361 │ POINT 7.274006698354081 46.275553205520794 │ │ CRUISE │ 29604 │ 34000 │ 79276 │ 0.78 │ 139 │ 363 │ POINT 8.243146568075773 46.75332543362598 │ │ CRUISE │ 30018 │ 36000 │ 78915 │ 0.78 │ 133 │ 361 │ POINT 9.229481862337197 47.22261290149568 │ │ CRUISE │ 30436 │ 38000 │ 78555 │ 0.78 │ 127 │ 360 │ POINT 10.23327610333648 47.68312559281413 │ │ CRUISE │ 30861 │ 38000 │ 78191 │ 0.78 │ 127 │ 365 │ POINT 11.25477083484032 48.13456566890824 │ │ CRUISE │ 31297 │ 36000 │ 77819 │ 0.78 │ 133 │ 371 │ POINT 12.29418112454121 48.57662718213212 │ │ CRUISE │ 31732 │ 34000 │ 77449 │ 0.78 │ 139 │ 370 │ POINT 13.351689351409924 49.00899540355703 │ │ CRUISE │ 32161 │ 32000 │ 77082 │ 0.78 │ 146 │ 367 │ POINT 14.427435465032865 49.431345252175966 │ │ CRUISE │ 32589 │ 30000 │ 76710 │ 0.78 │ 152 │ 372 │ POINT 15.521498988740307 49.843337488922174 │ │ DESCENT │ 33101 │ 24066 │ 76280 │ 0.65 │ 142 │ 429 │ POINT 16.633858546975723 50.2446086742223 │ │ DESCENT │ 33589 │ 18131 │ 75861 │ 0.65 │ 160 │ 420 │ POINT 17.764276668345886 50.63474030548783 │ │ DESCENT │ 34046 │ 12197 │ 75433 │ 0.65 │ 179 │ 428 │ POINT 18.91184798374883 51.01313503252613 │ │ DESCENT │ 34472 │ 6262 │ 74984 │ 0.65 │ 199 │ 449 │ POINT 20.071843186874247 51.378167839584854 │ │ DESCENT │ 34954 │ 100 │ 74400 │ 0.65 │ 221 │ 584 │ POINT 20.94663 52.17147 │ └─────────┴───────┴───────┴───────┴────────┴───────┴───────┴───────────────────────────────────────────────┘ Airbus A320 vs Boeing 737 OpenTop can be paired with OpenAP and used to figure out flight trajectories between two airports. Its optimiser requires a grid cost file. I'll first download an example 142 MB NetCDF file provided by the project. bash $ wget https://opendap.4tu.nl/thredds/fileServer/data2/djht/bea8a3fe-e34c-4598-9f94-c5a5c63348e5/1/contrail original.nc The cost file can be either in Casadi or Parquet format. I worked from an example https://openap.dev/optimize/contrails.html in its documentation, which produced a 246 KB Casadi file. python import openap import pandas as pd from scipy.ndimage import gaussian filter from opentop.tools import cached interpolant from dataframe import xarray as xr ds = xr.open dataset 'contrail original.nc' \ .sel time='2015-12-18' level pressure = 0.0000, 10.0000, 30.0000, 50.0000, 70.0000, 90.0787, 110.6606, 132.3968, 155.7909, 181.1544, 208.6494, 238.3258, 270.1530, 304.0465, 339.8891, 377.5467, 416.8789, 457.7442, 500.0000, 543.4970, 588.0685, 633.5144, 679.5799, 725.9285, 772.1102, 817.5241, 861.3757, 902.6287, 939.9520, 971.6610, 995.6532, 1009.3396 df = ds.to dataframe .reset index .assign lev=lambda x: x.lev.astype int .merge pd.DataFrame level pressure, columns= "hPa" .reset index names="lev" , on="lev", .assign height=lambda x: openap.aero.h isa x.hPa 100 .round -2 .assign longitude=lambda x: x.lon + 180 % 360 - 180 .query "height<15000" df cost world = df.rename columns={ "lat": "latitude", "atr20 contrail": "cost", } "time", "latitude", "longitude", "hPa", "height", "cost" df cost = df cost world.query "-20