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