pycontrails 0.58.0__cp314-cp314-macosx_10_13_x86_64.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.
- pycontrails/__init__.py +70 -0
- pycontrails/_version.py +34 -0
- pycontrails/core/__init__.py +30 -0
- pycontrails/core/aircraft_performance.py +679 -0
- pycontrails/core/airports.py +228 -0
- pycontrails/core/cache.py +889 -0
- pycontrails/core/coordinates.py +174 -0
- pycontrails/core/fleet.py +483 -0
- pycontrails/core/flight.py +2185 -0
- pycontrails/core/flightplan.py +228 -0
- pycontrails/core/fuel.py +140 -0
- pycontrails/core/interpolation.py +702 -0
- pycontrails/core/met.py +2931 -0
- pycontrails/core/met_var.py +387 -0
- pycontrails/core/models.py +1321 -0
- pycontrails/core/polygon.py +549 -0
- pycontrails/core/rgi_cython.cpython-314-darwin.so +0 -0
- pycontrails/core/vector.py +2249 -0
- pycontrails/datalib/__init__.py +12 -0
- pycontrails/datalib/_met_utils/metsource.py +746 -0
- pycontrails/datalib/ecmwf/__init__.py +73 -0
- pycontrails/datalib/ecmwf/arco_era5.py +345 -0
- pycontrails/datalib/ecmwf/common.py +114 -0
- pycontrails/datalib/ecmwf/era5.py +554 -0
- pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
- pycontrails/datalib/ecmwf/hres.py +804 -0
- pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
- pycontrails/datalib/ecmwf/ifs.py +287 -0
- pycontrails/datalib/ecmwf/model_levels.py +435 -0
- pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
- pycontrails/datalib/ecmwf/variables.py +268 -0
- pycontrails/datalib/geo_utils.py +261 -0
- pycontrails/datalib/gfs/__init__.py +28 -0
- pycontrails/datalib/gfs/gfs.py +656 -0
- pycontrails/datalib/gfs/variables.py +104 -0
- pycontrails/datalib/goes.py +757 -0
- pycontrails/datalib/himawari/__init__.py +27 -0
- pycontrails/datalib/himawari/header_struct.py +266 -0
- pycontrails/datalib/himawari/himawari.py +667 -0
- pycontrails/datalib/landsat.py +589 -0
- pycontrails/datalib/leo_utils/__init__.py +5 -0
- pycontrails/datalib/leo_utils/correction.py +266 -0
- pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
- pycontrails/datalib/leo_utils/search.py +250 -0
- pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
- pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
- pycontrails/datalib/leo_utils/vis.py +59 -0
- pycontrails/datalib/sentinel.py +650 -0
- pycontrails/datalib/spire/__init__.py +5 -0
- pycontrails/datalib/spire/exceptions.py +62 -0
- pycontrails/datalib/spire/spire.py +604 -0
- pycontrails/ext/bada.py +42 -0
- pycontrails/ext/cirium.py +14 -0
- pycontrails/ext/empirical_grid.py +140 -0
- pycontrails/ext/synthetic_flight.py +431 -0
- pycontrails/models/__init__.py +1 -0
- pycontrails/models/accf.py +425 -0
- pycontrails/models/apcemm/__init__.py +8 -0
- pycontrails/models/apcemm/apcemm.py +983 -0
- pycontrails/models/apcemm/inputs.py +226 -0
- pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
- pycontrails/models/apcemm/utils.py +437 -0
- pycontrails/models/cocip/__init__.py +29 -0
- pycontrails/models/cocip/cocip.py +2742 -0
- pycontrails/models/cocip/cocip_params.py +305 -0
- pycontrails/models/cocip/cocip_uncertainty.py +291 -0
- pycontrails/models/cocip/contrail_properties.py +1530 -0
- pycontrails/models/cocip/output_formats.py +2270 -0
- pycontrails/models/cocip/radiative_forcing.py +1260 -0
- pycontrails/models/cocip/radiative_heating.py +520 -0
- pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
- pycontrails/models/cocip/wake_vortex.py +396 -0
- pycontrails/models/cocip/wind_shear.py +120 -0
- pycontrails/models/cocipgrid/__init__.py +9 -0
- pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
- pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
- pycontrails/models/dry_advection.py +602 -0
- pycontrails/models/emissions/__init__.py +21 -0
- pycontrails/models/emissions/black_carbon.py +599 -0
- pycontrails/models/emissions/emissions.py +1353 -0
- pycontrails/models/emissions/ffm2.py +336 -0
- pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
- pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
- pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
- pycontrails/models/extended_k15.py +1327 -0
- pycontrails/models/humidity_scaling/__init__.py +37 -0
- pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
- pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
- pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
- pycontrails/models/issr.py +210 -0
- pycontrails/models/pcc.py +326 -0
- pycontrails/models/pcr.py +154 -0
- pycontrails/models/ps_model/__init__.py +18 -0
- pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
- pycontrails/models/ps_model/ps_grid.py +701 -0
- pycontrails/models/ps_model/ps_model.py +1000 -0
- pycontrails/models/ps_model/ps_operational_limits.py +525 -0
- pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
- pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
- pycontrails/models/sac.py +442 -0
- pycontrails/models/tau_cirrus.py +183 -0
- pycontrails/physics/__init__.py +1 -0
- pycontrails/physics/constants.py +117 -0
- pycontrails/physics/geo.py +1138 -0
- pycontrails/physics/jet.py +968 -0
- pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
- pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
- pycontrails/physics/thermo.py +551 -0
- pycontrails/physics/units.py +472 -0
- pycontrails/py.typed +0 -0
- pycontrails/utils/__init__.py +1 -0
- pycontrails/utils/dependencies.py +66 -0
- pycontrails/utils/iteration.py +13 -0
- pycontrails/utils/json.py +187 -0
- pycontrails/utils/temp.py +50 -0
- pycontrails/utils/types.py +163 -0
- pycontrails-0.58.0.dist-info/METADATA +180 -0
- pycontrails-0.58.0.dist-info/RECORD +122 -0
- pycontrails-0.58.0.dist-info/WHEEL +6 -0
- pycontrails-0.58.0.dist-info/licenses/LICENSE +178 -0
- pycontrails-0.58.0.dist-info/licenses/NOTICE +43 -0
- 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
|