How to estimate the fuel burnt by an aircraft?

The traffic library integrates the OpenAP aircraft performance and emission model.

We can demonstrate its use with an example flight extracted from a real flight data recorder of an Airbus A320-216 aircraft. Data is anonymised, so there is no geographical information about the trajectory and timestamps are just wrong.

About anonymisation

  • I may consider adding extra features to this dataset if it could help validate or illustrate a use case with the library. The bottom line is that anonymisation (tail number) should not be obviously broken.

  • If you find a way to break the anonymisation of this particular flight, good for you: nobody will confirm or deny your claim.

  • You may manage to find the city-pair associated with that flight: if you reconstruct the whole trajectory with a reasonable process, let’s write a tutorial page to illustrate it.

from traffic.data.samples import fuelflow_a320

There are enough features (and some more) in the provided data to estimate the fuel flow with OpenAP and compare the result with real data.

fuelflow_a320.data[['timestamp', 'altitude', 'groundspeed', 'CAS', 'vertical_acceleration', 'weight', 'fuelflow']]
timestamp altitude groundspeed CAS vertical_acceleration weight fuelflow
602 2011-07-23 13:23:09+00:00 232 169 164.875 1.195312 69454.064722 7625.792472
603 2011-07-23 13:23:10+00:00 264 169 165.000 1.121094 69454.064722 7642.121792
604 2011-07-23 13:23:11+00:00 296 169 165.125 1.121094 69454.064722 7643.936161
605 2011-07-23 13:23:12+00:00 330 169 164.625 1.035156 69454.064722 7634.864316
606 2011-07-23 13:23:13+00:00 364 169 162.625 0.964844 69444.992874 7629.421210
... ... ... ... ... ... ... ...
12405 2011-07-23 16:39:52+00:00 156 135 128.125 0.976562 60926.528040 720.304452
12406 2011-07-23 16:39:53+00:00 156 134 126.875 1.015625 60926.528040 733.005034
12407 2011-07-23 16:39:54+00:00 164 132 124.375 0.929688 60926.528040 912.627555
12408 2011-07-23 16:39:55+00:00 172 130 123.750 1.187500 60917.456192 1043.262115
12409 2011-07-23 16:39:56+00:00 170 127 120.875 1.140625 60908.384344 1491.411233

11808 rows × 7 columns

User interface

The most direct use of the API, is with the fuelflow() method:

Flight.fuelflow(initial_mass=None, typecode=None, engine=None)

Estimates the fuel flow with OpenAP.

The OpenAP model is based on the aircraft type (actually, the most probable engine type) and on three features commonly available in ADS-B data:

  • altitude (in ft),

  • vertical rate (in ft/min), and

  • speed (in kts), in order of priority, TAS (true air speed), CAS (computed air speed, used to compute TAS) and groundspeed, if no air speed is available.

Parameters:
  • initial_mass (Union[None, str, float]) – by default (None), 90% of the maximum take-off weight. You can also pass a value to initialise the mass. When initial_mass > 1, the mass is in kg. When initial_mass <= 1, it represents the fraction of the maximum take-off weight.

  • typecode (Optional[str]) – by default (None), use the typecode column if available, the provided aircraft database to infer the typecode based on the icao24. Ignored if the engine parameter is not None.

  • engine (Optional[str]) – by default (None), use the default engine associated with the aircraft type.

Return type:

Flight

Returns:

the same instance enriched with three extra features: the mass, the fuel flow (in kg/s) and the total burnt fuel (in kg).

In this dataset:

  • the vertical rate is not available as it is not directly measured on aircraft. Here, we consider the most simple approach and derive it from the altitude.
    In practice, the vertical acceleration is used to filter the vertical rate signal. It has been made available in this dataset (as a g-force)
  • the fuel flow (the ground truth) is provided in kg/h, we convert it in kg/s for compatibility reason with the output of the OpenAP interface.

f = fuelflow_a320.assign(
    # the vertical_rate is not present in the data
    vertical_rate=lambda df: df.altitude.diff().fillna(0) * 60,
    # convert to kg/s
    fuelflow=lambda df: df.fuelflow / 3600,
)

Sources of uncertainty

Let’s analyse the results produced with the following runs.

import altair as alt
alt.data_transformers.disable_max_rows()


def plot_flow(flight):
    return flight.chart().encode(
        alt.X(
            "utchoursminutesseconds(timestamp)",
            axis=alt.Axis(title=None, format="%H:%M"),
        ),
        alt.Y("fuelflow", axis=alt.Axis(title="fuel flow (in kg/s)")),
        alt.Color("legend", title=None),
    )


def chart_flow(*flights):
    return (
        alt.layer(*(plot_flow(flight) for flight in flights))
        .properties(width=600, height=250)
        .configure_axis(
            labelFontSize=14, titleFontSize=16,
            titleAngle=0, titleY=-12, titleAnchor="start",
        )
        .configure_legend(
            orient="bottom", columns=1,
            labelFontSize=14, symbolSize=400, symbolStrokeWidth=3,
        )
    )

Default parameters

The default approach considers the default engine (which is the correct one for this particular aircraft), assumes the initial mass of the aircraft to be 90% of the initial take-off mass, and computes the TAS based on the available CAS.

The typecode="A320" must be passed as a parameter because the icao24 parameter is not provided in this example.

resampled = f.resample("5s")
openap = resampled.fuelflow(typecode="A320")

chart_flow(
    openap.assign(legend="OpenAP estimation"),
    resampled.assign(legend="Real fuelflow")
)
real_fuel = resampled.weight_max - resampled.weight_min
estimated_fuel = openap.fuel_max

print(f"Total burnt fuel: {real_fuel:.0f}kg, OpenAP estimation: {estimated_fuel:.0f}kg")
print(f"Error: {abs(estimated_fuel - real_fuel) / real_fuel:.0%}")
Total burnt fuel: 8582kg, OpenAP estimation: 6862kg
Error: 20%

Impact of the take-off mass

As the weight of the aircraft is available along this particular trajectory—note that this is most likely an approximation too, based on the quantity of fuel loaded, the estimation of fuel burnt, cargo, number of embarked passengers, etc.—we can see that a better estimation of the mass slightly improves the estimation of the fuel flow.

resampled = f.resample("5s")
openap = resampled.fuelflow(typecode="A320", initial_mass=resampled.weight_max)

chart_flow(
    openap.assign(legend="OpenAP estimation"),
    resampled.assign(legend="Real fuelflow")
)
real_fuel = resampled.weight_max - resampled.weight_min
estimated_fuel = openap.fuel_max

print(f"Total burnt fuel: {real_fuel:.0f}kg, OpenAP estimation: {estimated_fuel:.0f}kg")
print(f"Error: {abs(estimated_fuel - real_fuel) / real_fuel:.0%}")
Total burnt fuel: 8582kg, OpenAP estimation: 6843kg
Error: 20%

Impact of the wind

On previous examples, OpenAP tends to overestimate the quantity of fuel burnt, by 13% in this example. However, ignoring the strong headwind makes an important difference: for this particular flight, the model underestimates the total burnt fuel by 26%.

resampled = f.resample("5s").drop(columns=["CAS"])
openap = resampled.fuelflow(typecode="A320")

chart_flow(
    openap.assign(legend="OpenAP estimation"),
    resampled.assign(legend="Real fuelflow")
)
real_fuel = resampled.weight_max - resampled.weight_min
estimated_fuel = openap.fuel_max

print(f"Total burnt fuel: {real_fuel:.0f}kg, OpenAP estimation: {estimated_fuel:.0f}kg")
print(f"Error: {abs(estimated_fuel - real_fuel) / real_fuel:.0%}")
Total burnt fuel: 8582kg, OpenAP estimation: 6890kg
Error: 20%

There are two ways to take wind into account when estimating fuel flow based on ADS-B data:

  • use information from extended Mode S in areas of the world where it is available (see query_ehs());

  • interpolate wind from GRIB files provided by Meteorological Agencies and use the information to compute the true air speed (TAS)

Influence of the sampling rate

The current Python implementation of fuel flow estimation is a bit slow, but changing the sampling rate of the trajectories in order to accelerate processing seems to have little impact on the final estimation.

resampled = f.resample("20s")
openap = resampled.fuelflow(typecode="A320")

chart_flow(
    openap.assign(legend="OpenAP estimation"),
    resampled.assign(legend="Real fuelflow")
)
real_fuel = resampled.weight_max - resampled.weight_min
estimated_fuel = openap.fuel_max

print(f"Total burnt fuel: {real_fuel:.0f}kg, OpenAP estimation: {estimated_fuel:.0f}kg")
print(f"Error: {abs(estimated_fuel - real_fuel) / real_fuel:.0%}")
Total burnt fuel: 8564kg, OpenAP estimation: 6897kg
Error: 19%

Influence of the engine type

The engine type has a serious impact on the fuel flow estimation even if the general trend looks similar. If you know the engine type for each aircraft, it may be more reasonable to specify it when running your estimation.

resampled = f.resample("5s")
openap = resampled.fuelflow(typecode="A320", engine="CFM56-5B5")  # default/correct is CFM56-5B4

chart_flow(
    openap.assign(legend="OpenAP estimation"),
    resampled.assign(legend="Real fuelflow")
)
real_fuel = resampled.weight_max - resampled.weight_min
estimated_fuel = openap.fuel_max

print(f"Total burnt fuel: {real_fuel:.0f}kg, OpenAP estimation: {estimated_fuel:.0f}kg")
print(f"Error: {abs(estimated_fuel - real_fuel) / real_fuel:.0%}")
Total burnt fuel: 8582kg, OpenAP estimation: 6796kg
Error: 21%