pycontrails 0.58.0__cp314-cp314-macosx_11_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pycontrails might be problematic. Click here for more details.

Files changed (122) hide show
  1. pycontrails/__init__.py +70 -0
  2. pycontrails/_version.py +34 -0
  3. pycontrails/core/__init__.py +30 -0
  4. pycontrails/core/aircraft_performance.py +679 -0
  5. pycontrails/core/airports.py +228 -0
  6. pycontrails/core/cache.py +889 -0
  7. pycontrails/core/coordinates.py +174 -0
  8. pycontrails/core/fleet.py +483 -0
  9. pycontrails/core/flight.py +2185 -0
  10. pycontrails/core/flightplan.py +228 -0
  11. pycontrails/core/fuel.py +140 -0
  12. pycontrails/core/interpolation.py +702 -0
  13. pycontrails/core/met.py +2931 -0
  14. pycontrails/core/met_var.py +387 -0
  15. pycontrails/core/models.py +1321 -0
  16. pycontrails/core/polygon.py +549 -0
  17. pycontrails/core/rgi_cython.cpython-314-darwin.so +0 -0
  18. pycontrails/core/vector.py +2249 -0
  19. pycontrails/datalib/__init__.py +12 -0
  20. pycontrails/datalib/_met_utils/metsource.py +746 -0
  21. pycontrails/datalib/ecmwf/__init__.py +73 -0
  22. pycontrails/datalib/ecmwf/arco_era5.py +345 -0
  23. pycontrails/datalib/ecmwf/common.py +114 -0
  24. pycontrails/datalib/ecmwf/era5.py +554 -0
  25. pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
  26. pycontrails/datalib/ecmwf/hres.py +804 -0
  27. pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
  28. pycontrails/datalib/ecmwf/ifs.py +287 -0
  29. pycontrails/datalib/ecmwf/model_levels.py +435 -0
  30. pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
  31. pycontrails/datalib/ecmwf/variables.py +268 -0
  32. pycontrails/datalib/geo_utils.py +261 -0
  33. pycontrails/datalib/gfs/__init__.py +28 -0
  34. pycontrails/datalib/gfs/gfs.py +656 -0
  35. pycontrails/datalib/gfs/variables.py +104 -0
  36. pycontrails/datalib/goes.py +757 -0
  37. pycontrails/datalib/himawari/__init__.py +27 -0
  38. pycontrails/datalib/himawari/header_struct.py +266 -0
  39. pycontrails/datalib/himawari/himawari.py +667 -0
  40. pycontrails/datalib/landsat.py +589 -0
  41. pycontrails/datalib/leo_utils/__init__.py +5 -0
  42. pycontrails/datalib/leo_utils/correction.py +266 -0
  43. pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
  44. pycontrails/datalib/leo_utils/search.py +250 -0
  45. pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
  46. pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
  47. pycontrails/datalib/leo_utils/vis.py +59 -0
  48. pycontrails/datalib/sentinel.py +650 -0
  49. pycontrails/datalib/spire/__init__.py +5 -0
  50. pycontrails/datalib/spire/exceptions.py +62 -0
  51. pycontrails/datalib/spire/spire.py +604 -0
  52. pycontrails/ext/bada.py +42 -0
  53. pycontrails/ext/cirium.py +14 -0
  54. pycontrails/ext/empirical_grid.py +140 -0
  55. pycontrails/ext/synthetic_flight.py +431 -0
  56. pycontrails/models/__init__.py +1 -0
  57. pycontrails/models/accf.py +425 -0
  58. pycontrails/models/apcemm/__init__.py +8 -0
  59. pycontrails/models/apcemm/apcemm.py +983 -0
  60. pycontrails/models/apcemm/inputs.py +226 -0
  61. pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
  62. pycontrails/models/apcemm/utils.py +437 -0
  63. pycontrails/models/cocip/__init__.py +29 -0
  64. pycontrails/models/cocip/cocip.py +2742 -0
  65. pycontrails/models/cocip/cocip_params.py +305 -0
  66. pycontrails/models/cocip/cocip_uncertainty.py +291 -0
  67. pycontrails/models/cocip/contrail_properties.py +1530 -0
  68. pycontrails/models/cocip/output_formats.py +2270 -0
  69. pycontrails/models/cocip/radiative_forcing.py +1260 -0
  70. pycontrails/models/cocip/radiative_heating.py +520 -0
  71. pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
  72. pycontrails/models/cocip/wake_vortex.py +396 -0
  73. pycontrails/models/cocip/wind_shear.py +120 -0
  74. pycontrails/models/cocipgrid/__init__.py +9 -0
  75. pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
  76. pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
  77. pycontrails/models/dry_advection.py +602 -0
  78. pycontrails/models/emissions/__init__.py +21 -0
  79. pycontrails/models/emissions/black_carbon.py +599 -0
  80. pycontrails/models/emissions/emissions.py +1353 -0
  81. pycontrails/models/emissions/ffm2.py +336 -0
  82. pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
  83. pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
  84. pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
  85. pycontrails/models/extended_k15.py +1327 -0
  86. pycontrails/models/humidity_scaling/__init__.py +37 -0
  87. pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
  88. pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
  89. pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
  90. pycontrails/models/issr.py +210 -0
  91. pycontrails/models/pcc.py +326 -0
  92. pycontrails/models/pcr.py +154 -0
  93. pycontrails/models/ps_model/__init__.py +18 -0
  94. pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
  95. pycontrails/models/ps_model/ps_grid.py +701 -0
  96. pycontrails/models/ps_model/ps_model.py +1000 -0
  97. pycontrails/models/ps_model/ps_operational_limits.py +525 -0
  98. pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
  99. pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
  100. pycontrails/models/sac.py +442 -0
  101. pycontrails/models/tau_cirrus.py +183 -0
  102. pycontrails/physics/__init__.py +1 -0
  103. pycontrails/physics/constants.py +117 -0
  104. pycontrails/physics/geo.py +1138 -0
  105. pycontrails/physics/jet.py +968 -0
  106. pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
  107. pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
  108. pycontrails/physics/thermo.py +551 -0
  109. pycontrails/physics/units.py +472 -0
  110. pycontrails/py.typed +0 -0
  111. pycontrails/utils/__init__.py +1 -0
  112. pycontrails/utils/dependencies.py +66 -0
  113. pycontrails/utils/iteration.py +13 -0
  114. pycontrails/utils/json.py +187 -0
  115. pycontrails/utils/temp.py +50 -0
  116. pycontrails/utils/types.py +163 -0
  117. pycontrails-0.58.0.dist-info/METADATA +180 -0
  118. pycontrails-0.58.0.dist-info/RECORD +122 -0
  119. pycontrails-0.58.0.dist-info/WHEEL +6 -0
  120. pycontrails-0.58.0.dist-info/licenses/LICENSE +178 -0
  121. pycontrails-0.58.0.dist-info/licenses/NOTICE +43 -0
  122. pycontrails-0.58.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,968 @@
1
+ """Jet aircraft trajectory and performance parameters.
2
+
3
+ This module includes common functions to calculate jet aircraft trajectory
4
+ and performance parameters, including fuel quantities, mass, thrust setting,
5
+ propulsion efficiency and load factors.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import functools
11
+ import logging
12
+ import pathlib
13
+
14
+ import numpy as np
15
+ import numpy.typing as npt
16
+ import pandas as pd
17
+
18
+ from pycontrails.core import flight
19
+ from pycontrails.physics import constants, units
20
+ from pycontrails.utils.types import ArrayOrFloat, ArrayScalarLike
21
+
22
+ logger = logging.getLogger(__name__)
23
+ _path_to_static = pathlib.Path(__file__).parent / "static"
24
+ PLF_PATH = _path_to_static / "iata-passenger-load-factors-20250221.csv"
25
+ CLF_PATH = _path_to_static / "iata-cargo-load-factors-20250221.csv"
26
+
27
+
28
+ # -------------------
29
+ # Aircraft performance
30
+ # -------------------
31
+
32
+
33
+ def acceleration(
34
+ true_airspeed: npt.NDArray[np.floating], segment_duration: npt.NDArray[np.floating]
35
+ ) -> npt.NDArray[np.floating]:
36
+ r"""Calculate the acceleration/deceleration at each waypoint.
37
+
38
+ Parameters
39
+ ----------
40
+ true_airspeed : npt.NDArray[np.floating]
41
+ True airspeed, [:math:`m \ s^{-1}`]
42
+ segment_duration : npt.NDArray[np.floating]
43
+ Time difference between waypoints, [:math:`s`]
44
+
45
+ Returns
46
+ -------
47
+ npt.NDArray[np.floating]
48
+ Acceleration/deceleration, [:math:`m \ s^{-2}`]
49
+
50
+ See Also
51
+ --------
52
+ pycontrails.Flight.segment_duration
53
+ """
54
+ dv_dt = np.empty_like(true_airspeed)
55
+ dv_dt[:-1] = np.diff(true_airspeed) / segment_duration[:-1]
56
+ dv_dt[-1] = 0.0
57
+ np.nan_to_num(dv_dt, copy=False)
58
+ return dv_dt
59
+
60
+
61
+ def climb_descent_angle(
62
+ true_airspeed: npt.NDArray[np.floating], rocd: npt.NDArray[np.floating]
63
+ ) -> npt.NDArray[np.floating]:
64
+ r"""Calculate angle between the horizontal plane and the actual flight path.
65
+
66
+ Parameters
67
+ ----------
68
+ true_airspeed : npt.NDArray[np.floating]
69
+ True airspeed, [:math:`m \ s^{-1}`]
70
+ rocd : npt.NDArray[np.floating]
71
+ Rate of climb/descent, [:math:`ft min^{-1}`]
72
+
73
+ Returns
74
+ -------
75
+ npt.NDArray[np.floating]
76
+ Climb (positive value) or descent (negative value) angle, [:math:`\deg`]
77
+
78
+ See Also
79
+ --------
80
+ pycontrails.Flight.segment_rocd
81
+ pycontrails.Flight.segment_true_airspeed
82
+ """
83
+ rocd_ms = units.ft_to_m(rocd) / 60.0
84
+ sin_theta = rocd_ms / true_airspeed
85
+ return units.radians_to_degrees(np.arcsin(sin_theta))
86
+
87
+
88
+ def clip_mach_number(
89
+ true_airspeed: npt.NDArray[np.floating],
90
+ air_temperature: npt.NDArray[np.floating],
91
+ max_mach_number: ArrayOrFloat,
92
+ ) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]:
93
+ r"""Compute the Mach number from the true airspeed and ambient temperature.
94
+
95
+ This method clips the computed Mach number to the value of ``max_mach_number``.
96
+
97
+ If no mach number exceeds ``max_mach_number``, the original array ``true_airspeed``
98
+ and the computed Mach number are returned.
99
+
100
+ Parameters
101
+ ----------
102
+ true_airspeed : npt.NDArray[np.floating]
103
+ Array of true airspeed, [:math:`m \ s^{-1}`]
104
+ air_temperature : npt.NDArray[np.floating]
105
+ Array of ambient temperature, [:math: `K`]
106
+ max_mach_number : ArrayOrFloat
107
+ Maximum mach number associated to aircraft. If no clipping
108
+ is desired, this can be set tp `np.inf`.
109
+
110
+ Returns
111
+ -------
112
+ true_airspeed : npt.NDArray[np.floating]
113
+ Array of true airspeed, [:math:`m \ s^{-1}`]. All values are clipped at
114
+ ``max_mach_number``.
115
+ mach_num : npt.NDArray[np.floating]
116
+ Array of Mach numbers, [:math:`Ma`]. All values are clipped at
117
+ ``max_mach_number``.
118
+ """
119
+ mach_num = units.tas_to_mach_number(true_airspeed, air_temperature)
120
+
121
+ is_unrealistic = mach_num > max_mach_number
122
+ if not np.any(is_unrealistic):
123
+ return true_airspeed, mach_num
124
+
125
+ msg = (
126
+ f"Unrealistic Mach numbers found. Discovered {np.sum(is_unrealistic)} / "
127
+ f"{is_unrealistic.size} values exceeding this, the largest of which "
128
+ f"is {np.nanmax(mach_num):.4f}. These are all clipped at {max_mach_number}."
129
+ )
130
+ logger.debug(msg)
131
+
132
+ max_tas = units.mach_number_to_tas(max_mach_number, air_temperature)
133
+ adjusted_mach_num = np.where(is_unrealistic, max_mach_number, mach_num)
134
+ adjusted_true_airspeed = np.where(is_unrealistic, max_tas, true_airspeed)
135
+
136
+ return adjusted_true_airspeed, adjusted_mach_num
137
+
138
+
139
+ def overall_propulsion_efficiency(
140
+ true_airspeed: npt.NDArray[np.floating],
141
+ F_thrust: npt.NDArray[np.floating],
142
+ fuel_flow: npt.NDArray[np.floating],
143
+ q_fuel: float,
144
+ is_descent: npt.NDArray[np.bool_] | bool | None,
145
+ threshold: float = 0.5,
146
+ ) -> npt.NDArray[np.floating]:
147
+ r"""Calculate the overall propulsion efficiency (OPE).
148
+
149
+ Negative OPE values can occur during the descent phase and is clipped to a
150
+ lower bound of 0, while an upper bound of ``threshold`` is also applied.
151
+ The most efficient engines today do not exceed this value.
152
+
153
+ Parameters
154
+ ----------
155
+ true_airspeed: npt.NDArray[np.floating]
156
+ True airspeed for each waypoint, [:math:`m s^{-1}`].
157
+ F_thrust: npt.NDArray[np.floating]
158
+ Thrust force provided by the engine, [:math:`N`].
159
+ fuel_flow: npt.NDArray[np.floating]
160
+ Fuel mass flow rate, [:math:`kg s^{-1}`].
161
+ q_fuel : float
162
+ Lower calorific value (LCV) of fuel, [:math:`J \ kg_{fuel}^{-1}`].
163
+ is_descent : npt.NDArray[np.floating] | None
164
+ Boolean array that indicates if a waypoint is in a descent phase.
165
+ threshold : float
166
+ Upper bound for realistic engine efficiency.
167
+
168
+ Returns
169
+ -------
170
+ npt.NDArray[np.floating]
171
+ Overall propulsion efficiency (OPE)
172
+
173
+ References
174
+ ----------
175
+ - :cite:`schumannConditionsContrailFormation1996`
176
+ - :cite:`cumpstyJetPropulsion2015`
177
+ """
178
+ ope = (F_thrust * true_airspeed) / (fuel_flow * q_fuel)
179
+ if is_descent is not None:
180
+ ope[is_descent] = 0.0
181
+
182
+ n_unrealistic = np.sum(ope > threshold)
183
+ if n_unrealistic:
184
+ logger.debug(
185
+ "Found %s engine efficiency values exceeding %s. These are clipped.",
186
+ n_unrealistic,
187
+ threshold,
188
+ )
189
+ ope.clip(0.0, threshold, out=ope) # clip in place
190
+ return ope
191
+
192
+
193
+ # -------------------
194
+ # Aircraft fuel quantities
195
+ # -------------------
196
+
197
+
198
+ def fuel_burn(
199
+ fuel_flow: npt.NDArray[np.floating], segment_duration: npt.NDArray[np.floating]
200
+ ) -> npt.NDArray[np.floating]:
201
+ """Calculate the fuel consumption at each waypoint.
202
+
203
+ Parameters
204
+ ----------
205
+ fuel_flow: npt.NDArray[np.floating]
206
+ Fuel mass flow rate, [:math:`kg s^{-1}`]
207
+ segment_duration: npt.NDArray[np.floating]
208
+ Time difference between waypoints, [:math:`s`]
209
+
210
+ Returns
211
+ -------
212
+ npt.NDArray[np.floating]
213
+ Fuel consumption at each waypoint, [:math:`kg`]
214
+ """
215
+ return fuel_flow * segment_duration
216
+
217
+
218
+ def equivalent_fuel_flow_rate_at_sea_level(
219
+ fuel_flow_cruise: npt.NDArray[np.floating],
220
+ theta_amb: npt.NDArray[np.floating],
221
+ delta_amb: npt.NDArray[np.floating],
222
+ mach_num: npt.NDArray[np.floating],
223
+ ) -> npt.NDArray[np.floating]:
224
+ r"""Convert fuel mass flow rate at cruise conditions to equivalent flow rate at sea level.
225
+
226
+ Refer to Eq. (40) in :cite:`duboisFuelFlowMethod22006`.
227
+
228
+ Parameters
229
+ ----------
230
+ fuel_flow_cruise : npt.NDArray[np.floating]
231
+ Fuel mass flow rate per engine, [:math:`kg s^{-1}`]
232
+ theta_amb : npt.NDArray[np.floating]
233
+ Ratio of the ambient temperature to the temperature at mean sea-level.
234
+ delta_amb : npt.NDArray[np.floating]
235
+ Ratio of the pressure altitude to the surface pressure.
236
+ mach_num : npt.NDArray[np.floating]
237
+ Mach number, [:math: `Ma`]
238
+
239
+ Returns
240
+ -------
241
+ npt.NDArray[np.floating]
242
+ Estimate of fuel flow per engine at sea level, [:math:`kg \ s^{-1}`].
243
+
244
+ References
245
+ ----------
246
+ - :cite:`duboisFuelFlowMethod22006`
247
+ """
248
+ return fuel_flow_cruise * (theta_amb**3.8 / delta_amb) * np.exp(0.2 * mach_num**2)
249
+
250
+
251
+ def equivalent_fuel_flow_rate_at_cruise(
252
+ fuel_flow_sls: npt.NDArray[np.floating] | float,
253
+ theta_amb: ArrayOrFloat,
254
+ delta_amb: ArrayOrFloat,
255
+ mach_num: ArrayOrFloat,
256
+ ) -> npt.NDArray[np.floating]:
257
+ r"""Convert fuel mass flow rate at sea level to equivalent fuel flow rate at cruise conditions.
258
+
259
+ Refer to Eq. (40) in :cite:`duboisFuelFlowMethod22006`.
260
+
261
+ Parameters
262
+ ----------
263
+ fuel_flow_sls : npt.NDArray[np.floating] | float
264
+ Fuel mass flow rate, [:math:`kg s^{-1}`]
265
+ theta_amb : ArrayOrFloat
266
+ Ratio of the ambient temperature to the temperature at mean sea-level.
267
+ delta_amb : ArrayOrFloat
268
+ Ratio of the pressure altitude to the surface pressure.
269
+ mach_num : ArrayOrFloat
270
+ Mach number
271
+
272
+ Returns
273
+ -------
274
+ npt.NDArray[np.floating]
275
+ Estimate of fuel mass flow rate at sea level, [:math:`kg \ s^{-1}`]
276
+
277
+ References
278
+ ----------
279
+ - :cite:`duboisFuelFlowMethod22006`
280
+ """
281
+ denom = (theta_amb**3.8 / delta_amb) * np.exp(0.2 * mach_num**2)
282
+ # denominator must be >= 1, otherwise corrected_fuel_flow > fuel_flow_max_sls
283
+ denom.clip(min=1.0, out=denom)
284
+ return fuel_flow_sls / denom
285
+
286
+
287
+ def reserve_fuel_requirements(
288
+ rocd: npt.NDArray[np.floating],
289
+ altitude_ft: npt.NDArray[np.floating],
290
+ fuel_flow: npt.NDArray[np.floating],
291
+ fuel_burn: npt.NDArray[np.floating],
292
+ ) -> float:
293
+ r"""
294
+ Estimate reserve fuel requirements.
295
+
296
+ Parameters
297
+ ----------
298
+ rocd: npt.NDArray[np.floating]
299
+ Rate of climb and descent, [:math:`ft \ min^{-1}`]
300
+ altitude_ft: npt.NDArray[np.floating]
301
+ Altitude, [:math:`ft`]
302
+ fuel_flow: npt.NDArray[np.floating]
303
+ Fuel mass flow rate, [:math:`kg \ s^{-1}`].
304
+ fuel_burn: npt.NDArray[np.floating]
305
+ Fuel consumption for each waypoint, [:math:`kg`]
306
+
307
+ Returns
308
+ -------
309
+ npt.NDArray[np.floating]
310
+ Reserve fuel requirements, [:math:`kg`]
311
+
312
+ References
313
+ ----------
314
+ - :cite:`wasiukAircraftPerformanceModel2015`
315
+
316
+ Notes
317
+ -----
318
+ The real-world calculation of the reserve fuel requirements is highly complex
319
+ (refer to Section 2.3.3 of :cite:`wasiukAircraftPerformanceModel2015`).
320
+ This implementation is simplified by taking the maximum between the following two conditions:
321
+
322
+ 1. Fuel required to fly +90 minutes at the main cruise altitude at the end of the
323
+ cruise aircraft weight.
324
+ 2. Uplift the total fuel consumption for the flight by +15%
325
+
326
+ See Also
327
+ --------
328
+ pycontrails.Flight.segment_phase
329
+ fuel_burn
330
+ """
331
+ segment_phase = flight.segment_phase(rocd, altitude_ft)
332
+
333
+ is_cruise = (segment_phase == flight.FlightPhase.CRUISE) & np.isfinite(fuel_flow)
334
+
335
+ # If there is no cruise phase, take the mean over the whole flight
336
+ if not np.any(is_cruise):
337
+ ff_end_of_cruise = np.nanmean(fuel_flow).item()
338
+
339
+ # Otherwise, take the average of the final 10 waypoints
340
+ else:
341
+ ff_end_of_cruise = np.mean(fuel_flow[is_cruise][-10:]).item()
342
+
343
+ reserve_fuel_1 = (90.0 * 60.0) * ff_end_of_cruise # 90 minutes at cruise fuel flow
344
+ reserve_fuel_2 = 0.15 * np.nansum(fuel_burn).item() # 15% uplift on total fuel burn
345
+
346
+ return max(reserve_fuel_1, reserve_fuel_2)
347
+
348
+
349
+ # -------------
350
+ # Aircraft mass
351
+ # -------------
352
+
353
+
354
+ @functools.cache
355
+ def _historical_regional_load_factor(path: pathlib.Path) -> pd.DataFrame:
356
+ """Load the historical regional load factor database.
357
+
358
+ Daily load factors are estimated from linearly interpolating the monthly statistics.
359
+
360
+ Returns
361
+ -------
362
+ pd.DataFrame
363
+ Historical regional load factor for each day.
364
+
365
+ Notes
366
+ -----
367
+ The monthly **passenger load factor** for each region is compiled from IATA's monthly
368
+ publication of the Air Passenger Market Analysis, where the static file will be continuously
369
+ updated. The report estimates the regional passenger load factor by dividing the revenue
370
+ passenger-km (RPK) by the available seat-km (ASK).
371
+
372
+ The monthly **cargo load factor** for each region is compiled from IATA's monthly publication
373
+ of the Air Cargo Market Analysis, where the static file will be continuously updated.
374
+ The report estimates the regional cargo load factor by dividing the freight tonne-km (FTK)
375
+ by the available freight tonne-km (AFTK).
376
+ """
377
+ df = pd.read_csv(path, index_col="Date", parse_dates=True, date_format="%d/%m/%Y")
378
+ return df.resample("D").interpolate()
379
+
380
+
381
+ AIRPORT_TO_REGION = {
382
+ "A": "Asia Pacific",
383
+ "B": "Europe",
384
+ "C": "North America",
385
+ "D": "Africa",
386
+ "E": "Europe",
387
+ "F": "Africa",
388
+ "G": "Africa",
389
+ "H": "Africa",
390
+ "K": "North America",
391
+ "L": "Europe",
392
+ "M": "Latin America",
393
+ "N": "Asia Pacific",
394
+ "O": "Middle East",
395
+ "P": "Asia Pacific",
396
+ "R": "Asia Pacific",
397
+ "S": "Latin America",
398
+ "T": "Latin America",
399
+ "U": "Asia Pacific",
400
+ "V": "Asia Pacific",
401
+ "W": "Asia Pacific",
402
+ "Y": "Asia Pacific",
403
+ "Z": "Asia Pacific",
404
+ }
405
+
406
+
407
+ def aircraft_load_factor(
408
+ origin_airport_icao: str | None = None,
409
+ first_waypoint_time: pd.Timestamp | None = None,
410
+ *,
411
+ freighter: bool = False,
412
+ ) -> float:
413
+ """
414
+ Estimate passenger/cargo load factor based on historical data.
415
+
416
+ Accounts for regional and seasonal differences.
417
+
418
+ Parameters
419
+ ----------
420
+ origin_airport_icao : str | None
421
+ ICAO code of origin airport. If None is provided, then globally averaged values will be
422
+ assumed at `first_waypoint_time`.
423
+ first_waypoint_time : pd.Timestamp | None
424
+ First waypoint UTC time. If None is provided, then regionally or globally averaged values
425
+ from the trailing twelve months will be used.
426
+ freighter: bool
427
+ Historical cargo load factor will be used if true, otherwise use passenger load factor.
428
+
429
+ Returns
430
+ -------
431
+ float
432
+ Passenger/cargo load factor [0 - 1], unitless
433
+ """
434
+ # If origin airport is provided, use regional load factor.
435
+ # Otherwise, do not allow empty string and `None` to pass
436
+ if origin_airport_icao:
437
+ first_letter = origin_airport_icao[0]
438
+ region = AIRPORT_TO_REGION.get(first_letter, "Global")
439
+ else:
440
+ region = "Global"
441
+
442
+ # Use passenger or cargo database
443
+ if freighter:
444
+ lf_database = _historical_regional_load_factor(CLF_PATH)
445
+ else:
446
+ lf_database = _historical_regional_load_factor(PLF_PATH)
447
+
448
+ # If `first_waypoint_time` is None, global/regional averages for the trailing twelve months
449
+ # will be assumed.
450
+ if first_waypoint_time is None:
451
+ t1 = lf_database.index[-1]
452
+ t0 = t1 - pd.DateOffset(months=12) + pd.DateOffset(days=1)
453
+ return lf_database.loc[t0:t1, region].mean().item()
454
+
455
+ date = first_waypoint_time.floor("D")
456
+
457
+ # If `date` is more recent than the historical data, then use most recent load factors
458
+ # from trailing twelve months as seasonal values are stable except in COVID years (2020-22).
459
+ if date > lf_database.index[-1]:
460
+ if date.month == 2 and date.day == 29: # remove any leap day
461
+ date = date.replace(day=28)
462
+
463
+ filt = (lf_database.index.month == date.month) & (lf_database.index.day == date.day)
464
+ date = lf_database.index[filt][-1]
465
+
466
+ # (2) If `date` is before the historical data, then use 2019 load factors.
467
+ elif date < lf_database.index[0]:
468
+ if date.month == 2 and date.day == 29: # remove any leap day
469
+ date = date.replace(day=28)
470
+
471
+ filt = (lf_database.index.month == date.month) & (lf_database.index.day == date.day)
472
+ date = lf_database.index[filt][0]
473
+
474
+ return lf_database.at[date, region].item()
475
+
476
+
477
+ def aircraft_weight(aircraft_mass: ArrayOrFloat) -> ArrayOrFloat:
478
+ """Calculate the aircraft weight at each waypoint.
479
+
480
+ Parameters
481
+ ----------
482
+ aircraft_mass : ArrayOrFloat
483
+ Aircraft mass, [:math:`kg`]
484
+
485
+ Returns
486
+ -------
487
+ ArrayOrFloat
488
+ Aircraft weight, [:math:`N`]
489
+ """
490
+ return aircraft_mass * constants.g
491
+
492
+
493
+ def initial_aircraft_mass(
494
+ *,
495
+ operating_empty_weight: float,
496
+ max_takeoff_weight: float,
497
+ max_payload: float,
498
+ total_fuel_burn: float,
499
+ total_reserve_fuel: float,
500
+ load_factor: float,
501
+ ) -> float:
502
+ """Estimate initial aircraft mass as a function of load factor and fuel requirements.
503
+
504
+ This function uses the following equation::
505
+
506
+ TOM = OEM + PM + FM_nc + TFM
507
+ = OEM + LF * MPM + FM_nc + TFM
508
+
509
+ where:
510
+ - TOM is the aircraft take-off mass
511
+ - OEM is the aircraft operating empty weight
512
+ - PM is the payload mass
513
+ - FM_nc is the mass of the fuel not consumed
514
+ - TFM is the trip fuel mass
515
+ - LF is the load factor
516
+ - MPM is the maximum payload mass
517
+
518
+ Parameters
519
+ ----------
520
+ operating_empty_weight: float
521
+ Aircraft operating empty weight, i.e. the basic weight of an aircraft including
522
+ the crew and necessary equipment, but excluding usable fuel and payload, [:math:`kg`]
523
+ max_takeoff_weight: float
524
+ Aircraft maximum take-off weight, [:math:`kg`]
525
+ max_payload: float
526
+ Aircraft maximum payload, [:math:`kg`]
527
+ total_fuel_burn: float
528
+ Total fuel consumption for the flight, obtained from prior iterations, [:math:`kg`]
529
+ total_reserve_fuel: float
530
+ Total reserve fuel requirements, [:math:`kg`]
531
+ load_factor: float
532
+ Aircraft load factor assumption (between 0 and 1)
533
+
534
+ Returns
535
+ -------
536
+ float
537
+ Aircraft mass at the initial waypoint, [:math:`kg`]
538
+
539
+ References
540
+ ----------
541
+ - :cite:`wasiukAircraftPerformanceModel2015`
542
+
543
+ See Also
544
+ --------
545
+ reserve_fuel_requirements
546
+ aircraft_load_factor
547
+ """
548
+ tom = operating_empty_weight + load_factor * max_payload + total_fuel_burn + total_reserve_fuel
549
+ return min(tom, max_takeoff_weight)
550
+
551
+
552
+ def update_aircraft_mass(
553
+ *,
554
+ operating_empty_weight: float,
555
+ max_takeoff_weight: float,
556
+ max_payload: float,
557
+ fuel_burn: npt.NDArray[np.floating],
558
+ total_reserve_fuel: float,
559
+ load_factor: float,
560
+ takeoff_mass: float | None,
561
+ ) -> npt.NDArray[np.floating]:
562
+ """Update aircraft mass based on the simulated total fuel consumption.
563
+
564
+ Used internally for finding aircraft mass iteratively.
565
+
566
+ Parameters
567
+ ----------
568
+ operating_empty_weight: float
569
+ Aircraft operating empty weight, i.e. the basic weight of an aircraft including
570
+ the crew and necessary equipment, but excluding usable fuel and payload, [:math:`kg`].
571
+ ref_mass: float
572
+ Aircraft reference mass, [:math:`kg`].
573
+ max_takeoff_weight: float
574
+ Aircraft maximum take-off weight, [:math:`kg`].
575
+ max_payload: float
576
+ Aircraft maximum payload, [:math:`kg`]
577
+ fuel_burn: npt.NDArray[np.floating]
578
+ Fuel consumption for each waypoint, [:math:`kg`]
579
+ total_reserve_fuel: float
580
+ Total reserve fuel requirements, [:math:`kg`]
581
+ load_factor: float
582
+ Aircraft load factor assumption (between 0 and 1). This is the ratio of the
583
+ actual payload weight to the maximum payload weight.
584
+ takeoff_mass: float | None
585
+ Initial aircraft mass, [:math:`kg`]. If None, the initial mass is calculated
586
+ using :func:`initial_aircraft_mass`. If supplied, all other parameters except
587
+ ``fuel_burn`` are ignored.
588
+
589
+ Returns
590
+ -------
591
+ npt.NDArray[np.floating]
592
+ Updated aircraft mass, [:math:`kg`]
593
+
594
+ See Also
595
+ --------
596
+ fuel_burn
597
+ reserve_fuel_requirements
598
+ initial_aircraft_mass
599
+ aircraft_load_factor
600
+ """
601
+ if takeoff_mass is None:
602
+ takeoff_mass = initial_aircraft_mass(
603
+ operating_empty_weight=operating_empty_weight,
604
+ max_takeoff_weight=max_takeoff_weight,
605
+ max_payload=max_payload,
606
+ total_fuel_burn=np.nansum(fuel_burn).item(),
607
+ total_reserve_fuel=total_reserve_fuel,
608
+ load_factor=load_factor,
609
+ )
610
+
611
+ # Calculate updated aircraft mass for each waypoint
612
+ amass = np.empty_like(fuel_burn)
613
+ amass[0] = takeoff_mass
614
+ amass[1:] = takeoff_mass - np.nancumsum(fuel_burn)[:-1]
615
+
616
+ return amass
617
+
618
+
619
+ # ------------------------------------------------------------------
620
+ # Temperature and pressure at different sections of the jet engine
621
+ # ------------------------------------------------------------------
622
+
623
+
624
+ def compressor_inlet_temperature(T: ArrayScalarLike, mach_num: ArrayScalarLike) -> ArrayScalarLike:
625
+ """Calculate compressor inlet temperature for Jet engine, :math:`T_{2}`.
626
+
627
+ Parameters
628
+ ----------
629
+ T : ArrayScalarLike
630
+ Ambient temperature, [:math:`K`]
631
+ mach_num : ArrayScalarLike
632
+ Mach number
633
+
634
+ Returns
635
+ -------
636
+ ArrayScalarLike
637
+ Compressor inlet temperature, [:math:`K`]
638
+
639
+ References
640
+ ----------
641
+ - :cite:`stettlerGlobalCivilAviation2013`
642
+ - :cite:`cumpstyJetPropulsion2015`
643
+ """
644
+ return T * (1.0 + ((constants.kappa - 1.0) / 2.0) * mach_num**2)
645
+
646
+
647
+ def compressor_inlet_pressure(p: ArrayScalarLike, mach_num: ArrayScalarLike) -> ArrayScalarLike:
648
+ """Calculate compressor inlet pressure for Jet engine, :math:`P_{2}`.
649
+
650
+ Parameters
651
+ ----------
652
+ p : ArrayScalarLike
653
+ Ambient pressure, [:math:`Pa`]
654
+ mach_num : ArrayScalarLike
655
+ Mach number
656
+
657
+ Returns
658
+ -------
659
+ ArrayScalarLike
660
+ Compressor inlet pressure, [:math:`Pa`]
661
+
662
+ References
663
+ ----------
664
+ - :cite:`stettlerGlobalCivilAviation2013`
665
+ - :cite:`cumpstyJetPropulsion2015`
666
+ """
667
+ power_term = constants.kappa / (constants.kappa - 1.0)
668
+ return p * (1.0 + ((constants.kappa - 1.0) / 2.0) * mach_num**2) ** power_term
669
+
670
+
671
+ def combustor_inlet_pressure(
672
+ pressure_ratio: float,
673
+ p_comp_inlet: ArrayScalarLike,
674
+ thrust_setting: ArrayScalarLike,
675
+ ) -> ArrayScalarLike:
676
+ """Calculate combustor inlet pressure, :math:`P_{3}`.
677
+
678
+ Parameters
679
+ ----------
680
+ pressure_ratio : float
681
+ Engine pressure ratio, unitless
682
+ p_comp_inlet : ArrayScalarLike
683
+ Compressor inlet pressure, [:math:`Pa`]
684
+ thrust_setting : ArrayScalarLike
685
+ Engine thrust setting, unitless
686
+
687
+ Returns
688
+ -------
689
+ ArrayScalarLike
690
+ Combustor inlet pressure, [:math:`Pa`]
691
+
692
+ References
693
+ ----------
694
+ - :cite:`stettlerGlobalCivilAviation2013`
695
+ - :cite:`cumpstyJetPropulsion2015`
696
+ """
697
+ return (p_comp_inlet * (pressure_ratio - 1.0) * thrust_setting) + p_comp_inlet
698
+
699
+
700
+ def combustor_inlet_temperature(
701
+ comp_efficiency: float,
702
+ T_comp_inlet: ArrayScalarLike,
703
+ p_comp_inlet: ArrayScalarLike,
704
+ p_comb_inlet: ArrayScalarLike,
705
+ ) -> ArrayScalarLike:
706
+ """Calculate combustor inlet temperature, :math:`T_{3}`.
707
+
708
+ Parameters
709
+ ----------
710
+ comp_efficiency : float
711
+ Engine compressor efficiency, [:math:`0 - 1`]
712
+ T_comp_inlet : ArrayScalarLike
713
+ Compressor inlet temperature, [:math:`K`]
714
+ p_comp_inlet : ArrayScalarLike
715
+ Compressor inlet pressure, [:math:`Pa`]
716
+ p_comb_inlet : ArrayScalarLike
717
+ Compressor inlet pressure, [:math:`Pa`]
718
+
719
+ Returns
720
+ -------
721
+ ArrayScalarLike
722
+ Combustor inlet temperature, [:math:`K`]
723
+
724
+ References
725
+ ----------
726
+ - :cite:`stettlerGlobalCivilAviation2013`
727
+ - :cite:`cumpstyJetPropulsion2015`
728
+ """
729
+ power_term = (constants.kappa - 1.0) / (constants.kappa * comp_efficiency)
730
+ return T_comp_inlet * (p_comb_inlet / p_comp_inlet) ** power_term
731
+
732
+
733
+ def turbine_inlet_temperature(
734
+ afr: ArrayScalarLike, T_comb_inlet: ArrayScalarLike, q_fuel: float
735
+ ) -> ArrayScalarLike:
736
+ r"""Calculate turbine inlet temperature, :math:`T_{4}`.
737
+
738
+ Parameters
739
+ ----------
740
+ afr : ArrayScalarLike
741
+ Air-to-fuel ratio, unitless
742
+ T_comb_inlet : ArrayScalarLike
743
+ Combustor inlet temperature, [:math:`K`]
744
+ q_fuel : float
745
+ Lower calorific value (LCV) of fuel, :math:`[J \ kg_{fuel}^{-1}]`
746
+
747
+ Returns
748
+ -------
749
+ ArrayScalarLike
750
+ Tubrine inlet temperature, [:math:`K`]
751
+
752
+ References
753
+ ----------
754
+ - :cite:`cumpstyJetPropulsion2015`
755
+ """
756
+ return (afr * constants.c_pd * T_comb_inlet + q_fuel) / (constants.c_p_combustion * (1.0 + afr))
757
+
758
+
759
+ # --------------------------------------
760
+ # Engine thrust force and thrust settings
761
+ # --------------------------------------
762
+
763
+
764
+ def thrust_force(
765
+ altitude: npt.NDArray[np.floating],
766
+ true_airspeed: npt.NDArray[np.floating],
767
+ segment_duration: npt.NDArray[np.floating],
768
+ aircraft_mass: npt.NDArray[np.floating],
769
+ F_drag: npt.NDArray[np.floating],
770
+ ) -> npt.NDArray[np.floating]:
771
+ r"""Calculate the thrust force at each waypoint.
772
+
773
+ Parameters
774
+ ----------
775
+ altitude : npt.NDArray[np.floating]
776
+ Waypoint altitude, [:math:`m`]
777
+ true_airspeed : npt.NDArray[np.floating]
778
+ True airspeed, [:math:`m \ s^{-1}`]
779
+ segment_duration : npt.NDArray[np.floating]
780
+ Time difference between waypoints, [:math:`s`]
781
+ aircraft_mass : npt.NDArray[np.floating]
782
+ Aircraft mass, [:math:`kg`]
783
+ F_drag : npt.NDArray[np.floating]
784
+ Draft force, [:math:`N`]
785
+
786
+ Returns
787
+ -------
788
+ npt.NDArray[np.floating]
789
+ Thrust force, [:math:`N`]
790
+
791
+ References
792
+ ----------
793
+ - :cite:`eurocontrolUSERMANUALBASE2010`
794
+
795
+ Notes
796
+ -----
797
+ The model balances the rate of forces acting on the aircraft
798
+ with the rate in increase in energy.
799
+
800
+ This estimate of thrust force is used in the BADA Total-Energy Model (Eq. 3.2-1).
801
+
802
+ Negative thrust must be corrected.
803
+ """
804
+ dh_dt = np.empty_like(altitude)
805
+ dh_dt[:-1] = np.diff(altitude) / segment_duration[:-1]
806
+ dh_dt[-1] = 0.0
807
+ np.nan_to_num(dh_dt, copy=False)
808
+
809
+ dv_dt = acceleration(true_airspeed, segment_duration)
810
+
811
+ return (
812
+ F_drag
813
+ + (aircraft_mass * constants.g * dh_dt + aircraft_mass * true_airspeed * dv_dt)
814
+ / true_airspeed
815
+ )
816
+
817
+
818
+ def thrust_setting_nd(
819
+ true_airspeed: ArrayScalarLike,
820
+ thrust_setting: ArrayScalarLike,
821
+ T: ArrayScalarLike,
822
+ p: ArrayScalarLike,
823
+ pressure_ratio: float,
824
+ q_fuel: float,
825
+ *,
826
+ comp_efficiency: float = 0.9,
827
+ cruise: bool = False,
828
+ ) -> ArrayScalarLike:
829
+ r"""Calculate the non-dimensionalized thrust setting of a Jet engine.
830
+
831
+ Result is in terms of the ratio of turbine inlet to the
832
+ compressor inlet temperature (t4_t2)
833
+
834
+ Parameters
835
+ ----------
836
+ true_airspeed : ArrayScalarLike
837
+ True airspeed, [:math:`m \ s^{-1}`]
838
+ thrust_setting : ArrayScalarLike
839
+ Engine thrust setting, unitless
840
+ T : ArrayScalarLike
841
+ Ambient temperature, [:math:`K`]
842
+ p : ArrayScalarLike
843
+ Ambient pressure, [:math:`Pa`]
844
+ pressure_ratio : float
845
+ Engine pressure ratio, unitless
846
+ q_fuel : float
847
+ Lower calorific value (LCV) of fuel, :math:`[J \ kg_{fuel}^{-1}]`
848
+ comp_efficiency : float, optional
849
+ Engine compressor efficiency, [:math:`0 - 1`].
850
+ Defaults to 0.9
851
+ cruise : bool, optional
852
+ Defaults to False
853
+
854
+ Returns
855
+ -------
856
+ ArrayScalarLike
857
+ Ratio of turbine inlet to the compressor inlet temperature, unitless
858
+
859
+ References
860
+ ----------
861
+ - :cite:`cumpstyJetPropulsion2015`
862
+ - :cite:`teohAviationContrailClimate2022`
863
+ """
864
+ mach_num = units.tas_to_mach_number(true_airspeed, T)
865
+ T_compressor_inlet = compressor_inlet_temperature(T, mach_num)
866
+ p_compressor_inlet = compressor_inlet_pressure(p, mach_num)
867
+ p_combustor_inlet = combustor_inlet_pressure(pressure_ratio, p_compressor_inlet, thrust_setting)
868
+ T_combustor_inlet = combustor_inlet_temperature(
869
+ comp_efficiency, T_compressor_inlet, p_compressor_inlet, p_combustor_inlet
870
+ )
871
+ afr = air_to_fuel_ratio(thrust_setting, cruise=cruise, T_compressor_inlet=T_compressor_inlet)
872
+ T_turbine_inlet = turbine_inlet_temperature(afr, T_combustor_inlet, q_fuel)
873
+ return T_turbine_inlet / T_compressor_inlet
874
+
875
+
876
+ def air_to_fuel_ratio(
877
+ thrust_setting: ArrayScalarLike,
878
+ *,
879
+ cruise: bool = False,
880
+ T_compressor_inlet: None | ArrayScalarLike = None,
881
+ ) -> ArrayScalarLike:
882
+ """Calculate air-to-fuel ratio from thrust setting.
883
+
884
+ Parameters
885
+ ----------
886
+ thrust_setting : ArrayScalarLike
887
+ Engine thrust setting, unitless
888
+ cruise : bool
889
+ Estimate thrust setting for cruise conditions. Defaults to False.
890
+ T_compressor_inlet : None | ArrayScalarLike
891
+ Compressor inlet temperature, [:math:`K`]
892
+ Required if ``cruise`` is True.
893
+ Defaults to None
894
+
895
+ Returns
896
+ -------
897
+ ArrayScalarLike
898
+ Air-to-fuel ratio, unitless
899
+
900
+ References
901
+ ----------
902
+ - :cite:`cumpstyJetPropulsion2015`
903
+ - AFR equation from :cite:`stettlerGlobalCivilAviation2013`
904
+ - Scaling factor to cruise from Eq. (30) of :cite:`duboisFuelFlowMethod22006`
905
+
906
+ """
907
+ afr = (0.0121 * thrust_setting + 0.008) ** (-1)
908
+
909
+ if not cruise:
910
+ return afr
911
+
912
+ if T_compressor_inlet is None:
913
+ raise ValueError("`T_compressor_inlet` is required when `cruise` is True")
914
+
915
+ return afr * (T_compressor_inlet / constants.T_msl)
916
+
917
+
918
+ # -------------------
919
+ # Atmospheric ratios
920
+ # -------------------
921
+
922
+
923
+ def temperature_ratio(T: npt.NDArray[np.floating]) -> npt.NDArray[np.floating]:
924
+ """Calculate the ratio of ambient temperature relative to the temperature at mean sea level.
925
+
926
+ Parameters
927
+ ----------
928
+ T : npt.NDArray[np.floating]
929
+ Air temperature, [:math:`K`]
930
+
931
+ Returns
932
+ -------
933
+ npt.NDArray[np.floating]
934
+ Ratio of the temperature to the temperature at mean sea-level (MSL).
935
+ """
936
+ return T / constants.T_msl
937
+
938
+
939
+ def pressure_ratio(p: npt.NDArray[np.floating]) -> npt.NDArray[np.floating]:
940
+ """Calculate the ratio of ambient pressure relative to the surface pressure.
941
+
942
+ Parameters
943
+ ----------
944
+ p : npt.NDArray[np.floating]
945
+ Air pressure, [:math:`Pa`]
946
+
947
+ Returns
948
+ -------
949
+ npt.NDArray[np.floating]
950
+ Ratio of the pressure altitude to the surface pressure.
951
+ """
952
+ return p / constants.p_surface
953
+
954
+
955
+ def density_ratio(rho: npt.NDArray[np.floating]) -> npt.NDArray[np.floating]:
956
+ r"""Calculate the ratio of air density relative to the air density at mean-sea-level.
957
+
958
+ Parameters
959
+ ----------
960
+ rho : npt.NDArray[np.floating]
961
+ Air density, [:math:`kg \ m^{3}`]
962
+
963
+ Returns
964
+ -------
965
+ npt.NDArray[np.floating]
966
+ Ratio of the density to the air density at mean sea-level (MSL).
967
+ """
968
+ return rho / constants.rho_msl