Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/load-factor-improvements #270

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion pycontrails/core/aircraft_performance.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from pycontrails.utils.types import ArrayOrFloat

#: Default load factor for aircraft performance models.
DEFAULT_LOAD_FACTOR = 0.7
DEFAULT_LOAD_FACTOR = 0.83


# --------------------------------------
Expand Down
173 changes: 173 additions & 0 deletions pycontrails/physics/jet.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,20 @@
from __future__ import annotations

import logging
import pathlib

import numpy as np
import pandas as pd
import numpy.typing as npt

from pycontrails.core import flight
from pycontrails.physics import constants, units
from pycontrails.utils.types import ArrayOrFloat, ArrayScalarLike

logger = logging.getLogger(__name__)
_path_to_static = pathlib.Path(__file__).parent / "static"
PLF_PATH = _path_to_static / "iata-passenger-load-factors-20241115.csv"
CLF_PATH = _path_to_static / "iata-cargo-load-factors-20241115.csv"
zebengberg marked this conversation as resolved.
Show resolved Hide resolved


# -------------------
Expand Down Expand Up @@ -344,6 +349,174 @@ def reserve_fuel_requirements(
# Aircraft mass
# -------------

def _historical_passenger_load_factor() -> pd.DataFrame:
"""Load historical regional passenger load factor database.

Returns
-------
pd.DataFrame
Historical regional passenger load factor for each day.

Notes
-----
The monthly passenger load factor for each region is compiled from IATA's monthly publication
of the Air Passenger Market Analysis, where the static file will be continuously updated.

The report estimates the regional passenger load factor by dividing the revenue passenger-km
(RPK) by the available seat-km (ASK).

The daily passenger load factor is estimated from linearly interpolating the monthly statistics.
"""
df = pd.read_csv(PLF_PATH)
df["Date"] = pd.to_datetime(df["Date"], format="%d/%m/%Y")
df.set_index("Date", inplace=True, drop=True)

# Interpolate monthly statistics to estimate daily load factors
dates = pd.date_range(start=df.index[0], end=df.index[-1], freq='1D')
df = df.reindex(dates)

# Fill NaN values with linear interpolation
df = df.interpolate(method='linear')
return df


def _historical_cargo_load_factor() -> pd.DataFrame:
"""Load historical regional cargo load factor database.

Returns
-------
pd.DataFrame
Historical regional cargo load factor for each day.

Notes
-----
The monthly cargo load factor for each region is compiled from IATA's monthly publication
of the Air Cargo Market Analysis, where the static file will be continuously updated.

The report estimates the regional cargo load factor by dividing the freight tonne-km (FTK)
by the available freight tonne-km (AFTK).

The daily cargo load factor is estimated from linearly interpolating the monthly statistics.
"""
df = pd.read_csv(CLF_PATH)
df["Date"] = pd.to_datetime(df["Date"], format="%d/%m/%Y")
df.set_index("Date", inplace=True, drop=True)

# Interpolate monthly statistics to estimate daily load factors
dates = pd.date_range(start=df.index[0], end=df.index[-1], freq='1D')
df = df.reindex(dates)

# Fill NaN values with linear interpolation
df = df.interpolate(method='linear')
return df


HISTORICAL_PLF = _historical_passenger_load_factor()
HISTORICAL_CLF = _historical_cargo_load_factor()

AIRPORT_TO_REGION = {
"A": "Asia Pacific",
"B": "Europe",
"C": "North America",
"D": "Africa",
"E": "Europe",
"F": "Africa",
"G": "Africa",
"H": "Africa",
"K": "North America",
"L": "Europe",
"M": "Latin America",
"N": "Asia Pacific",
"O": "Middle East",
"P": "Asia Pacific",
"R": "Asia Pacific",
"S": "Latin America",
"T": "Latin America",
"U": "Asia Pacific",
"V": "Asia Pacific",
"W": "Asia Pacific",
"Y": "Asia Pacific",
"Z": "Asia Pacific",
}


def aircraft_load_factor(
origin_airport_icao: str | None = None,
first_waypoint_time: pd.Timestamp | None = None,
freighter: bool = False,
) -> float:
"""
Estimate passenger/cargo load factor based on historical data.

Accounts for regional and seasonal differences.

Parameters
----------
origin_airport_icao : str | None
ICAO code of origin airport. If None is provided, then globally averaged values will be
assumed at `first_waypoint_time`.
first_waypoint_time : pd.Timestamp | None
First waypoint UTC time. If None is provided, then regionally or globally averaged values
from the trailing twelve months will be used.
freighter: bool
Historical cargo load factor will be used if true, otherwise use passenger load factor.

Returns
-------
float
Passenger/cargo load factor [0 - 1], unitless
"""
region = "Global"

# Use passenger or cargo database
if freighter:
lf_database = HISTORICAL_CLF
else:
lf_database = HISTORICAL_PLF

# If `first_waypoint_time` is None, global/regional averages for the trailing twelve months
# will be assumed.
if first_waypoint_time is None:
filt = lf_database.index > (lf_database.index[-1] - pd.DateOffset(months=12))
ttm = lf_database[filt].copy()
return np.nanmean(ttm[region].to_numpy())

date = first_waypoint_time.floor('D')

# If origin airport is provided, use regional load factor
if origin_airport_icao is not None:
first_letter = origin_airport_icao[0]
region = AIRPORT_TO_REGION.get(first_letter, "Global")

# If `date` is more recent than the historical data, then use most recent load factors
# from trailing twelve months as seasonal values are stable except in COVID years (2020-22).
if date > lf_database.index[-1]:
# Check for leap year
if date.month == 2 and date.day == 29:
date = date.replace(day=28)

filt = lf_database.index > (lf_database.index[-1] - pd.DateOffset(months=12))
ttm = lf_database[filt].copy()
ttm['mm_dd'] = ttm.index.strftime('%m-%d')

date_mm_dd = date.strftime('%m-%d')
date = pd.to_datetime(ttm.loc[ttm['mm_dd'] == date_mm_dd].index[0])

# (2) If `date` is before the historical data, then use 2019 load factors.
if date < lf_database.index[0]:
# Check for leap year
if date.month == 2 and date.day == 29:
date = date.replace(day=28)

filt = lf_database.index < (lf_database.index[0] + pd.DateOffset(months=12))
ftm = lf_database[filt].copy()
ftm['mm_dd'] = ftm.index.strftime('%m-%d')

date_mm_dd = date.strftime('%m-%d')
date = pd.to_datetime(ftm.loc[ftm['mm_dd'] == date_mm_dd].index[0])

return lf_database.loc[date, region]


def aircraft_weight(aircraft_mass: ArrayOrFloat) -> ArrayOrFloat:
"""Calculate the aircraft weight at each waypoint.
Expand Down
71 changes: 71 additions & 0 deletions pycontrails/physics/static/iata-cargo-load-factors-20241115.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
Date,Global,Africa,Asia Pacific,Europe,Latin America,Middle East,North America
15/12/2018,0.488,0.381,0.54,0.567,0.291,0.488,0.414
15/1/2019,0.451,0.354,0.501,0.501,0.299,0.421,0.4
15/2/2019,0.447,0.363,0.473,0.53,0.297,0.466,0.379
15/3/2019,0.495,0.384,0.556,0.56,0.323,0.488,0.416
15/4/2019,0.463,0.374,0.518,0.496,0.325,0.458,0.405
15/5/2019,0.468,0.386,0.52,0.513,0.353,0.469,0.398
15/6/2019,0.454,0.324,0.522,0.498,0.337,0.44,0.382
15/7/2019,0.45,0.323,0.519,0.485,0.354,0.453,0.373
15/8/2019,0.446,0.302,0.516,0.477,0.372,0.435,0.377
15/9/2019,0.464,0.329,0.539,0.501,0.379,0.459,0.381
15/10/2019,0.477,0.361,0.539,0.533,0.364,0.477,0.394
15/11/2019,0.496,0.404,0.538,0.569,0.403,0.497,0.413
15/12/2019,0.467,0.368,0.519,0.53,0.3,0.47,0.395
15/1/2020,0.45,0.356,0.474,0.501,0.311,0.426,0.424
15/2/2020,0.464,0.368,0.543,0.531,0.342,0.461,0.372
15/3/2020,0.545,0.425,0.656,0.63,0.411,0.532,0.429
15/4/2020,0.58,0.486,0.691,0.648,0.554,0.525,0.487
15/5/2020,0.576,0.612,0.643,0.625,0.561,0.483,0.526
15/6/2020,0.573,0.547,0.645,0.62,0.512,0.494,0.521
15/7/2020,0.564,0.489,0.639,0.594,0.464,0.53,0.506
15/8/2020,0.548,0.502,0.616,0.568,0.478,0.535,0.489
15/9/2020,0.569,0.507,0.642,0.62,0.456,0.579,0.484
15/10/2020,0.576,0.502,0.617,0.651,0.443,0.606,0.496
15/11/2020,0.582,0.496,0.631,0.655,0.436,0.6,0.5
15/12/2020,0.573,0.51,0.639,0.653,0.367,0.597,0.482
15/1/2021,0.589,0.48,0.665,0.627,0.39,0.569,0.532
15/2/2021,0.575,0.476,0.692,0.641,0.429,0.598,0.453
15/3/2021,0.588,0.499,0.661,0.685,0.453,0.613,0.472
15/4/2021,0.578,0.504,0.633,0.681,0.457,0.598,0.473
15/5/2021,0.572,0.502,0.646,0.656,0.423,0.589,0.469
15/6/2021,0.565,0.48,0.676,0.626,0.381,0.581,0.458
15/7/2021,0.544,0.455,0.654,0.598,0.387,0.536,0.443
15/8/2021,0.542,0.43,0.698,0.575,0.404,0.529,0.437
15/9/2021,0.553,0.428,0.68,0.604,0.37,0.558,0.447
15/10/2021,0.561,0.45,0.661,0.626,0.421,0.572,0.449
15/11/2021,0.559,0.434,0.654,0.631,0.446,0.572,0.444
15/12/2021,0.542,0.502,0.634,0.623,0.413,0.556,0.43
15/1/2022,0.541,0.492,0.609,0.584,0.417,0.513,0.474
15/2/2022,0.532,0.502,0.592,0.636,0.476,0.529,0.429
15/3/2022,0.549,0.494,0.638,0.671,0.448,0.526,0.442
15/4/2022,0.516,0.49,0.631,0.578,0.419,0.504,0.419
15/5/2022,0.505,0.495,0.627,0.548,0.387,0.487,0.411
15/6/2022,0.492,0.447,0.608,0.507,0.383,0.488,0.404
15/7/2022,0.472,0.452,0.563,0.493,0.374,0.469,0.398
15/8/2022,0.467,0.418,0.547,0.502,0.374,0.466,0.393
15/9/2022,0.481,0.451,0.572,0.528,0.381,0.478,0.396
15/10/2022,0.487,0.437,0.561,0.558,0.384,0.48,0.401
15/11/2022,0.491,0.458,0.545,0.569,0.382,0.475,0.419
15/12/2022,0.472,0.432,0.528,0.559,0.322,0.454,0.406
15/1/2023,0.448,0.439,0.452,0.541,0.325,0.411,0.423
15/2/2023,0.456,0.468,0.464,0.574,0.361,0.445,0.4
15/3/2023,0.462,0.489,0.485,0.57,0.366,0.456,0.393
15/4/2023,0.427,0.482,0.442,0.497,0.364,0.431,0.373
15/5/2023,0.415,0.448,0.422,0.489,0.333,0.41,0.373
15/6/2023,0.432,0.446,0.468,0.476,0.337,0.446,0.374
15/7/2023,0.421,0.417,0.457,0.472,0.322,0.411,0.37
15/8/2023,0.42,0.388,0.443,0.484,0.326,0.407,0.377
15/9/2023,0.438,0.436,0.466,0.5,0.319,0.424,0.392
15/10/2023,0.452,0.416,0.472,0.53,0.354,0.46,0.392
15/11/2023,0.467,0.421,0.479,0.57,0.363,0.469,0.408
15/12/2023,0.459,0.41,0.479,0.562,0.316,0.455,0.403
15/1/2024,0.457,0.431,0.446,0.555,0.344,0.439,0.435
15/2/2024,0.451,0.451,0.432,0.584,0.376,0.463,0.396
15/3/2024,0.473,0.473,0.475,0.581,0.402,0.496,0.404
15/4/2024,0.439,0.429,0.445,0.515,0.387,0.447,0.387
15/5/2024,0.446,0.438,0.453,0.518,0.362,0.461,0.397
15/6/2024,0.458,0.385,0.496,0.507,0.336,0.473,0.388
15/7/2024,0.444,0.4,0.48,0.496,0.338,0.458,0.382
15/8/2024,0.44,0.378,0.466,0.501,0.359,0.445,0.387
15/9/2024,0.456,0.392,0.485,0.525,0.368,0.474,0.389
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
Date,Global,Africa,Asia Pacific,Europe,Latin America,Middle East,North America
15/12/2018,0.804,0.724,0.81,0.81,0.818,0.736,0.825
15/1/2019,0.796,0.709,0.81,0.796,0.825,0.76,0.795
15/2/2019,0.806,0.704,0.826,0.815,0.813,0.726,0.808
15/3/2019,0.817,0.72,0.812,0.837,0.815,0.739,0.85
15/4/2019,0.828,0.733,0.817,0.851,0.822,0.803,0.839
15/5/2019,0.815,0.676,0.802,0.837,0.832,0.732,0.851
15/6/2019,0.844,0.706,0.821,0.875,0.832,0.767,0.887
15/7/2019,0.857,0.735,0.831,0.89,0.853,0.812,0.888
15/8/2019,0.857,0.755,0.839,0.889,0.833,0.821,0.875
15/9/2019,0.819,0.721,0.801,0.866,0.819,0.75,0.828
15/10/2019,0.82,0.697,0.815,0.855,0.819,0.734,0.841
15/11/2019,0.818,0.708,0.813,0.833,0.822,0.732,0.828
15/12/2019,0.823,0.724,0.816,0.828,0.825,0.78,0.859
15/1/2020,0.803,0.702,0.799,0.816,0.826,0.785,0.812
15/2/2020,0.759,0.668,0.678,0.813,0.812,0.725,0.811
15/3/2020,0.606,0.609,0.589,0.67,0.681,0.599,0.557
15/4/2020,0.366,0.111,0.538,0.32,0.55,0.284,0.15
15/5/2020,0.507,0.071,0.62,0.427,0.623,0.255,0.381
15/6/2020,0.576,0.162,0.638,0.555,0.666,0.357,0.524
15/7/2020,0.579,0.296,0.657,0.609,0.631,0.396,0.476
15/8/2020,0.585,0.39,0.65,0.635,0.639,0.372,0.477
15/9/2020,0.601,0.378,0.692,0.586,0.706,0.365,0.525
15/10/2020,0.602,0.482,0.687,0.552,0.721,0.387,0.558
15/11/2020,0.58,0.474,0.664,0.523,0.74,0.372,0.518
15/12/2020,0.575,0.549,0.616,0.578,0.73,0.44,0.516
15/1/2021,0.541,0.544,0.566,0.576,0.685,0.422,0.484
15/2/2021,0.554,0.516,0.591,0.563,0.683,0.398,0.527
15/3/2021,0.623,0.53,0.669,0.593,0.708,0.422,0.624
15/4/2021,0.633,0.476,0.678,0.563,0.723,0.404,0.668
15/5/2021,0.658,0.53,0.678,0.593,0.768,0.389,0.728
15/6/2021,0.696,0.587,0.657,0.658,0.784,0.459,0.806
15/7/2021,0.731,0.614,0.675,0.725,0.793,0.513,0.841
15/8/2021,0.7,0.64,0.545,0.746,0.774,0.56,0.786
15/9/2021,0.676,0.56,0.605,0.719,0.773,0.524,0.727
15/10/2021,0.706,0.558,0.629,0.741,0.809,0.577,0.769
15/11/2021,0.713,0.616,0.597,0.752,0.822,0.616,0.786
15/12/2021,0.723,0.647,0.625,0.745,0.816,0.663,0.793
15/1/2022,0.645,0.623,0.576,0.682,0.782,0.591,0.663
15/2/2022,0.698,0.648,0.629,0.721,0.795,0.648,0.745
15/3/2022,0.747,0.657,0.642,0.739,0.808,0.718,0.839
15/4/2022,0.778,0.68,0.67,0.795,0.809,0.713,0.858
15/5/2022,0.794,0.696,0.696,0.807,0.807,0.762,0.86
15/6/2022,0.824,0.743,0.729,0.86,0.817,0.772,0.891
15/7/2022,0.835,0.753,0.764,0.87,0.831,0.812,0.882
15/8/2022,0.818,0.757,0.74,0.862,0.824,0.796,0.856
15/9/2022,0.816,0.743,0.747,0.847,0.823,0.795,0.855
15/10/2022,0.82,0.726,0.755,0.848,0.833,0.791,0.864
15/11/2022,0.808,0.748,0.77,0.838,0.82,0.775,0.832
15/12/2022,0.811,0.769,0.772,0.836,0.785,0.8,0.842
15/1/2023,0.777,0.742,0.774,0.762,0.813,0.791,0.784
15/2/2023,0.778,0.756,0.792,0.752,0.811,0.798,0.771
15/3/2023,0.807,0.739,0.792,0.805,0.812,0.794,0.837
15/4/2023,0.813,0.708,0.784,0.838,0.814,0.76,0.856
15/5/2023,0.818,0.699,0.773,0.848,0.811,0.799,0.863
15/6/2023,0.842,0.689,0.804,0.877,0.825,0.794,0.887
15/7/2023,0.852,0.746,0.816,0.877,0.867,0.821,0.897
15/8/2023,0.846,0.764,0.822,0.876,0.851,0.83,0.858
15/9/2023,0.826,0.731,0.8,0.86,0.839,0.816,0.83
15/10/2023,0.831,0.707,0.821,0.856,0.848,0.806,0.836
15/11/2023,0.818,0.704,0.814,0.837,0.844,0.777,0.827
15/12/2023,0.821,0.732,0.812,0.851,0.827,0.782,0.829
15/1/2024,0.799,0.731,0.808,0.782,0.85,0.799,0.799
15/2/2024,0.806,0.744,0.844,0.761,0.827,0.808,0.795
15/3/2024,0.82,0.721,0.835,0.809,0.831,0.775,0.837
15/4/2024,0.824,0.734,0.824,0.838,0.822,0.792,0.83
15/5/2024,0.834,0.729,0.818,0.852,0.834,0.808,0.858
15/6/2024,0.85,0.771,0.829,0.877,0.842,0.795,0.876
15/7/2024,0.86,0.75,0.834,0.882,0.862,0.84,0.889
15/8/2024,0.862,0.779,0.86,0.879,0.84,0.823,0.871
15/9/2024,0.836,0.765,0.831,0.865,0.834,0.814,0.824
Loading
Loading