pycontrails 0.58.0__cp314-cp314-win_amd64.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.cp314-win_amd64.pyd +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 +5 -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,1000 @@
|
|
|
1
|
+
"""Support for the Poll-Schumann (PS) aircraft performance model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import functools
|
|
7
|
+
import pathlib
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
if sys.version_info >= (3, 12):
|
|
13
|
+
from typing import override
|
|
14
|
+
else:
|
|
15
|
+
from typing_extensions import override
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import numpy.typing as npt
|
|
19
|
+
import pandas as pd
|
|
20
|
+
|
|
21
|
+
from pycontrails.core import flight
|
|
22
|
+
from pycontrails.core.aircraft_performance import (
|
|
23
|
+
DEFAULT_LOAD_FACTOR,
|
|
24
|
+
AircraftPerformance,
|
|
25
|
+
AircraftPerformanceData,
|
|
26
|
+
AircraftPerformanceParams,
|
|
27
|
+
)
|
|
28
|
+
from pycontrails.core.flight import Flight
|
|
29
|
+
from pycontrails.core.met import MetDataset
|
|
30
|
+
from pycontrails.models.ps_model import ps_operational_limits as ps_lims
|
|
31
|
+
from pycontrails.models.ps_model.ps_aircraft_params import (
|
|
32
|
+
PSAircraftEngineParams,
|
|
33
|
+
load_aircraft_engine_params,
|
|
34
|
+
)
|
|
35
|
+
from pycontrails.physics import constants, jet, units
|
|
36
|
+
from pycontrails.utils.types import ArrayOrFloat
|
|
37
|
+
|
|
38
|
+
# mypy: disable-error-code = "type-var, arg-type"
|
|
39
|
+
|
|
40
|
+
#: Path to the Poll-Schumann aircraft parameters CSV file.
|
|
41
|
+
PS_SYNONYM_FILE_PATH = pathlib.Path(__file__).parent / "static" / "ps-synonym-list-20250328.csv"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclasses.dataclass
|
|
45
|
+
class PSFlightParams(AircraftPerformanceParams):
|
|
46
|
+
"""Default parameters for :class:`PSFlight`."""
|
|
47
|
+
|
|
48
|
+
#: Clip the ratio of the overall propulsion efficiency to the maximum propulsion
|
|
49
|
+
#: efficiency to always exceed this value.
|
|
50
|
+
eta_over_eta_b_min: float | None = 0.5
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class PSFlight(AircraftPerformance):
|
|
54
|
+
"""Simulate aircraft performance using Poll-Schumann (PS) model.
|
|
55
|
+
|
|
56
|
+
References
|
|
57
|
+
----------
|
|
58
|
+
:cite:`pollEstimationMethodFuel2021`
|
|
59
|
+
:cite:`pollEstimationMethodFuel2021a`
|
|
60
|
+
|
|
61
|
+
Poll & Schumann (2022). An estimation method for the fuel burn and other performance
|
|
62
|
+
characteristics of civil transport aircraft. Part 3 Generalisation to cover climb,
|
|
63
|
+
descent and holding. Aero. J., submitted.
|
|
64
|
+
|
|
65
|
+
See Also
|
|
66
|
+
--------
|
|
67
|
+
pycontrails.physics.jet.aircraft_load_factor
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
name = "PSFlight"
|
|
71
|
+
long_name = "Poll-Schumann Aircraft Performance Model"
|
|
72
|
+
default_params = PSFlightParams
|
|
73
|
+
|
|
74
|
+
aircraft_engine_params: Mapping[str, PSAircraftEngineParams]
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
met: MetDataset | None = None,
|
|
79
|
+
params: dict[str, Any] | None = None,
|
|
80
|
+
**params_kwargs: Any,
|
|
81
|
+
) -> None:
|
|
82
|
+
super().__init__(met=met, params=params, **params_kwargs)
|
|
83
|
+
self.aircraft_engine_params = load_aircraft_engine_params(
|
|
84
|
+
self.params["engine_deterioration_factor"]
|
|
85
|
+
)
|
|
86
|
+
self.synonym_dict = get_aircraft_synonym_dict_ps()
|
|
87
|
+
|
|
88
|
+
def check_aircraft_type_availability(
|
|
89
|
+
self, aircraft_type: str, raise_error: bool = True
|
|
90
|
+
) -> bool:
|
|
91
|
+
"""Check if aircraft type designator is available in the PS model database.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
aircraft_type : str
|
|
96
|
+
ICAO aircraft type designator
|
|
97
|
+
raise_error : bool, optional
|
|
98
|
+
Optional flag for raising an error, by default True.
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
bool
|
|
103
|
+
Aircraft found in the PS model database.
|
|
104
|
+
|
|
105
|
+
Raises
|
|
106
|
+
------
|
|
107
|
+
KeyError
|
|
108
|
+
raises KeyError if the aircraft type is not covered by database
|
|
109
|
+
"""
|
|
110
|
+
if aircraft_type in self.aircraft_engine_params or aircraft_type in self.synonym_dict:
|
|
111
|
+
return True
|
|
112
|
+
if raise_error:
|
|
113
|
+
msg = f"Aircraft type {aircraft_type} not covered by the PS model."
|
|
114
|
+
raise KeyError(msg)
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
@override
|
|
118
|
+
def eval_flight(self, fl: Flight) -> Flight:
|
|
119
|
+
# Ensure aircraft type is available
|
|
120
|
+
try:
|
|
121
|
+
aircraft_type = fl.get_constant("aircraft_type")
|
|
122
|
+
except KeyError as exc:
|
|
123
|
+
msg = "`aircraft_type` required on flight attrs"
|
|
124
|
+
raise KeyError(msg) from exc
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
atyp_ps = self.synonym_dict.get(aircraft_type) or aircraft_type
|
|
128
|
+
aircraft_params = self.aircraft_engine_params[atyp_ps]
|
|
129
|
+
except KeyError as exc:
|
|
130
|
+
msg = f"Aircraft type {aircraft_type} not covered by the PS model."
|
|
131
|
+
raise KeyError(msg) from exc
|
|
132
|
+
|
|
133
|
+
# Set flight attributes based on engine, if they aren't already defined
|
|
134
|
+
fl.attrs.setdefault("aircraft_performance_model", self.name)
|
|
135
|
+
fl.attrs.setdefault("aircraft_type_ps", atyp_ps)
|
|
136
|
+
fl.attrs.setdefault("n_engine", aircraft_params.n_engine)
|
|
137
|
+
|
|
138
|
+
fl.attrs.setdefault("wingspan", aircraft_params.wing_span)
|
|
139
|
+
fl.attrs.setdefault("max_mach", aircraft_params.max_mach_num)
|
|
140
|
+
fl.attrs.setdefault("max_altitude", units.ft_to_m(aircraft_params.fl_max * 100.0))
|
|
141
|
+
fl.attrs.setdefault("n_engine", aircraft_params.n_engine)
|
|
142
|
+
|
|
143
|
+
amass_oew = fl.attrs.get("amass_oew", aircraft_params.amass_oew)
|
|
144
|
+
amass_mtow = fl.attrs.get("amass_mtow", aircraft_params.amass_mtow)
|
|
145
|
+
amass_mpl = fl.attrs.get("amass_mpl", aircraft_params.amass_mpl)
|
|
146
|
+
load_factor = fl.attrs.get("load_factor", DEFAULT_LOAD_FACTOR)
|
|
147
|
+
takeoff_mass = fl.attrs.get("takeoff_mass")
|
|
148
|
+
q_fuel = fl.fuel.q_fuel
|
|
149
|
+
|
|
150
|
+
true_airspeed = fl["true_airspeed"] # attached in PSFlight.eval
|
|
151
|
+
true_airspeed = np.where(true_airspeed == 0.0, np.nan, true_airspeed)
|
|
152
|
+
|
|
153
|
+
# Run the simulation
|
|
154
|
+
aircraft_performance = self.simulate_fuel_and_performance(
|
|
155
|
+
aircraft_type=atyp_ps,
|
|
156
|
+
altitude_ft=fl.altitude_ft,
|
|
157
|
+
time=fl["time"],
|
|
158
|
+
true_airspeed=true_airspeed,
|
|
159
|
+
air_temperature=fl["air_temperature"],
|
|
160
|
+
aircraft_mass=self.get_data_param(fl, "aircraft_mass", None),
|
|
161
|
+
thrust=self.get_data_param(fl, "thrust", None),
|
|
162
|
+
engine_efficiency=self.get_data_param(fl, "engine_efficiency", None),
|
|
163
|
+
fuel_flow=self.get_data_param(fl, "fuel_flow", None),
|
|
164
|
+
q_fuel=q_fuel,
|
|
165
|
+
n_iter=self.params["n_iter"],
|
|
166
|
+
amass_oew=amass_oew,
|
|
167
|
+
amass_mtow=amass_mtow,
|
|
168
|
+
amass_mpl=amass_mpl,
|
|
169
|
+
load_factor=load_factor,
|
|
170
|
+
takeoff_mass=takeoff_mass,
|
|
171
|
+
correct_fuel_flow=self.params["correct_fuel_flow"],
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Set array aircraft_performance to flight, don't overwrite
|
|
175
|
+
for var in (
|
|
176
|
+
"aircraft_mass",
|
|
177
|
+
"engine_efficiency",
|
|
178
|
+
"fuel_flow",
|
|
179
|
+
"fuel_burn",
|
|
180
|
+
"thrust",
|
|
181
|
+
"rocd",
|
|
182
|
+
):
|
|
183
|
+
fl.setdefault(var, getattr(aircraft_performance, var))
|
|
184
|
+
|
|
185
|
+
self._cleanup_indices()
|
|
186
|
+
|
|
187
|
+
fl.attrs["total_fuel_burn"] = np.nansum(aircraft_performance.fuel_burn).item()
|
|
188
|
+
|
|
189
|
+
return fl
|
|
190
|
+
|
|
191
|
+
@override
|
|
192
|
+
def calculate_aircraft_performance(
|
|
193
|
+
self,
|
|
194
|
+
*,
|
|
195
|
+
aircraft_type: str,
|
|
196
|
+
altitude_ft: npt.NDArray[np.floating],
|
|
197
|
+
air_temperature: npt.NDArray[np.floating],
|
|
198
|
+
time: npt.NDArray[np.datetime64] | None,
|
|
199
|
+
true_airspeed: npt.NDArray[np.floating] | float | None,
|
|
200
|
+
aircraft_mass: npt.NDArray[np.floating] | float,
|
|
201
|
+
engine_efficiency: npt.NDArray[np.floating] | float | None,
|
|
202
|
+
fuel_flow: npt.NDArray[np.floating] | float | None,
|
|
203
|
+
thrust: npt.NDArray[np.floating] | float | None,
|
|
204
|
+
q_fuel: float,
|
|
205
|
+
**kwargs: Any,
|
|
206
|
+
) -> AircraftPerformanceData:
|
|
207
|
+
try:
|
|
208
|
+
correct_fuel_flow = kwargs["correct_fuel_flow"]
|
|
209
|
+
except KeyError as exc:
|
|
210
|
+
msg = "A 'correct_fuel_flow' kwarg is required for this model"
|
|
211
|
+
raise KeyError(msg) from exc
|
|
212
|
+
|
|
213
|
+
if not isinstance(true_airspeed, np.ndarray):
|
|
214
|
+
msg = "Only array inputs are supported"
|
|
215
|
+
raise NotImplementedError(msg)
|
|
216
|
+
|
|
217
|
+
atyp_param = self.aircraft_engine_params[aircraft_type]
|
|
218
|
+
|
|
219
|
+
# Atmospheric quantities
|
|
220
|
+
air_pressure = units.ft_to_pl(altitude_ft) * 100.0
|
|
221
|
+
|
|
222
|
+
# Clip unrealistically high true airspeed
|
|
223
|
+
max_mach = ps_lims.max_mach_number_by_altitude(
|
|
224
|
+
altitude_ft,
|
|
225
|
+
air_pressure,
|
|
226
|
+
atyp_param.max_mach_num,
|
|
227
|
+
atyp_param.p_i_max,
|
|
228
|
+
atyp_param.p_inf_co,
|
|
229
|
+
atm_speed_limit=False,
|
|
230
|
+
buffer=0.02,
|
|
231
|
+
)
|
|
232
|
+
true_airspeed, mach_num = jet.clip_mach_number(true_airspeed, air_temperature, max_mach)
|
|
233
|
+
|
|
234
|
+
# Reynolds number
|
|
235
|
+
rn = reynolds_number(atyp_param.wing_surface_area, mach_num, air_temperature, air_pressure)
|
|
236
|
+
|
|
237
|
+
# Allow array or None time
|
|
238
|
+
dv_dt: npt.NDArray[np.floating] | float
|
|
239
|
+
theta: npt.NDArray[np.floating] | float
|
|
240
|
+
if time is None:
|
|
241
|
+
# Assume a nominal cruising state
|
|
242
|
+
dt_sec = None
|
|
243
|
+
rocd = np.zeros_like(altitude_ft)
|
|
244
|
+
dv_dt = 0.0
|
|
245
|
+
theta = 0.0
|
|
246
|
+
|
|
247
|
+
elif isinstance(time, np.ndarray):
|
|
248
|
+
dt_sec = flight.segment_duration(time, dtype=altitude_ft.dtype)
|
|
249
|
+
rocd = flight.segment_rocd(dt_sec, altitude_ft, air_temperature)
|
|
250
|
+
dv_dt = jet.acceleration(true_airspeed, dt_sec)
|
|
251
|
+
theta = jet.climb_descent_angle(true_airspeed, rocd)
|
|
252
|
+
|
|
253
|
+
else:
|
|
254
|
+
msg = "Only array inputs are supported"
|
|
255
|
+
raise NotImplementedError(msg)
|
|
256
|
+
|
|
257
|
+
# Aircraft performance parameters
|
|
258
|
+
c_lift = lift_coefficient(
|
|
259
|
+
atyp_param.wing_surface_area, aircraft_mass, air_pressure, mach_num, theta
|
|
260
|
+
)
|
|
261
|
+
c_f = skin_friction_coefficient(rn)
|
|
262
|
+
c_drag_0 = zero_lift_drag_coefficient(c_f, atyp_param.psi_0)
|
|
263
|
+
e_ls = oswald_efficiency_factor(c_drag_0, atyp_param)
|
|
264
|
+
c_drag_w = wave_drag_coefficient(mach_num, c_lift, atyp_param)
|
|
265
|
+
c_drag = airframe_drag_coefficient(
|
|
266
|
+
c_drag_0, c_drag_w, c_lift, e_ls, atyp_param.wing_aspect_ratio
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Engine parameters and fuel consumption
|
|
270
|
+
if thrust is None:
|
|
271
|
+
thrust = thrust_force(aircraft_mass, c_lift, c_drag, dv_dt, theta)
|
|
272
|
+
|
|
273
|
+
c_t = engine_thrust_coefficient(
|
|
274
|
+
thrust, mach_num, air_pressure, atyp_param.wing_surface_area
|
|
275
|
+
)
|
|
276
|
+
c_t_eta_b = thrust_coefficient_at_max_efficiency(
|
|
277
|
+
mach_num, atyp_param.m_des, atyp_param.c_t_des
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if correct_fuel_flow:
|
|
281
|
+
c_t_available = ps_lims.max_available_thrust_coefficient(
|
|
282
|
+
air_temperature, mach_num, c_t_eta_b, atyp_param
|
|
283
|
+
)
|
|
284
|
+
np.clip(c_t, 0.0, c_t_available, out=c_t)
|
|
285
|
+
|
|
286
|
+
if engine_efficiency is None:
|
|
287
|
+
engine_efficiency = overall_propulsion_efficiency(
|
|
288
|
+
mach_num, c_t, c_t_eta_b, atyp_param, self.params["eta_over_eta_b_min"]
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if fuel_flow is None:
|
|
292
|
+
fuel_flow = fuel_mass_flow_rate(
|
|
293
|
+
air_pressure,
|
|
294
|
+
air_temperature,
|
|
295
|
+
mach_num,
|
|
296
|
+
c_t,
|
|
297
|
+
engine_efficiency,
|
|
298
|
+
atyp_param.wing_surface_area,
|
|
299
|
+
q_fuel,
|
|
300
|
+
)
|
|
301
|
+
elif isinstance(fuel_flow, int | float):
|
|
302
|
+
fuel_flow = np.full_like(true_airspeed, fuel_flow)
|
|
303
|
+
|
|
304
|
+
# Flight phase
|
|
305
|
+
segment_duration = flight.segment_duration(time, dtype=altitude_ft.dtype)
|
|
306
|
+
rocd = flight.segment_rocd(segment_duration, altitude_ft, air_temperature)
|
|
307
|
+
|
|
308
|
+
if correct_fuel_flow:
|
|
309
|
+
flight_phase = flight.segment_phase(rocd, altitude_ft)
|
|
310
|
+
fuel_flow = fuel_flow_correction(
|
|
311
|
+
fuel_flow,
|
|
312
|
+
altitude_ft,
|
|
313
|
+
air_temperature,
|
|
314
|
+
air_pressure,
|
|
315
|
+
mach_num,
|
|
316
|
+
atyp_param.ff_idle_sls,
|
|
317
|
+
atyp_param.ff_max_sls,
|
|
318
|
+
flight_phase,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if dt_sec is not None:
|
|
322
|
+
fuel_burn = jet.fuel_burn(fuel_flow, dt_sec)
|
|
323
|
+
else:
|
|
324
|
+
fuel_burn = np.full_like(fuel_flow, np.nan)
|
|
325
|
+
|
|
326
|
+
# XXX: Explicitly broadcast scalar inputs as needed to keep a consistent
|
|
327
|
+
# output spec.
|
|
328
|
+
if isinstance(aircraft_mass, int | float):
|
|
329
|
+
aircraft_mass = np.full_like(true_airspeed, aircraft_mass)
|
|
330
|
+
if isinstance(engine_efficiency, int | float):
|
|
331
|
+
engine_efficiency = np.full_like(true_airspeed, engine_efficiency)
|
|
332
|
+
if isinstance(thrust, int | float):
|
|
333
|
+
thrust = np.full_like(true_airspeed, thrust)
|
|
334
|
+
|
|
335
|
+
return AircraftPerformanceData(
|
|
336
|
+
fuel_flow=fuel_flow,
|
|
337
|
+
aircraft_mass=aircraft_mass,
|
|
338
|
+
true_airspeed=true_airspeed,
|
|
339
|
+
thrust=thrust,
|
|
340
|
+
fuel_burn=fuel_burn,
|
|
341
|
+
engine_efficiency=engine_efficiency,
|
|
342
|
+
rocd=rocd,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# ----------------------
|
|
347
|
+
# Atmospheric parameters
|
|
348
|
+
# ----------------------
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def reynolds_number(
|
|
352
|
+
wing_surface_area: float,
|
|
353
|
+
mach_num: ArrayOrFloat,
|
|
354
|
+
air_temperature: ArrayOrFloat,
|
|
355
|
+
air_pressure: ArrayOrFloat,
|
|
356
|
+
) -> ArrayOrFloat:
|
|
357
|
+
"""
|
|
358
|
+
Calculate Reynolds number.
|
|
359
|
+
|
|
360
|
+
Parameters
|
|
361
|
+
----------
|
|
362
|
+
wing_surface_area : float
|
|
363
|
+
Aircraft wing surface area, [:math:`m^2`]
|
|
364
|
+
mach_num : ArrayOrFloat
|
|
365
|
+
Mach number at each waypoint
|
|
366
|
+
air_temperature : ArrayOrFloat
|
|
367
|
+
Ambient temperature at each waypoint, [:math:`K`]
|
|
368
|
+
air_pressure: ArrayOrFloat
|
|
369
|
+
Ambient pressure, [:math:`Pa`]
|
|
370
|
+
|
|
371
|
+
Returns
|
|
372
|
+
-------
|
|
373
|
+
ArrayOrFloat
|
|
374
|
+
Reynolds number at each waypoint
|
|
375
|
+
|
|
376
|
+
References
|
|
377
|
+
----------
|
|
378
|
+
Eq. (3) of :cite:`pollEstimationMethodFuel2021`.
|
|
379
|
+
"""
|
|
380
|
+
mu = fluid_dynamic_viscosity(air_temperature)
|
|
381
|
+
return (
|
|
382
|
+
wing_surface_area**0.5
|
|
383
|
+
* mach_num
|
|
384
|
+
* (air_pressure / mu)
|
|
385
|
+
* (constants.kappa / (constants.R_d * air_temperature)) ** 0.5
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def fluid_dynamic_viscosity(air_temperature: ArrayOrFloat) -> ArrayOrFloat:
|
|
390
|
+
"""
|
|
391
|
+
Calculate fluid dynamic viscosity.
|
|
392
|
+
|
|
393
|
+
Parameters
|
|
394
|
+
----------
|
|
395
|
+
air_temperature : ArrayOrFloat
|
|
396
|
+
Ambient temperature at each waypoint, [:math:`K`]
|
|
397
|
+
|
|
398
|
+
Returns
|
|
399
|
+
-------
|
|
400
|
+
ArrayOrFloat
|
|
401
|
+
Fluid dynamic viscosity, [:math:`kg m^{-1} s^{-1}`]
|
|
402
|
+
|
|
403
|
+
Notes
|
|
404
|
+
-----
|
|
405
|
+
The dynamic viscosity is a measure of the fluid's resistance to flow and is represented
|
|
406
|
+
by Sutherland's Law. The higher the viscosity, the thicker the fluid.
|
|
407
|
+
|
|
408
|
+
References
|
|
409
|
+
----------
|
|
410
|
+
Eq. (25) of :cite:`pollEstimationMethodFuel2021`.
|
|
411
|
+
"""
|
|
412
|
+
return 1.458e-6 * (air_temperature**1.5) / (110.4 + air_temperature)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# -------------------------------
|
|
416
|
+
# Lift and drag coefficients
|
|
417
|
+
# -------------------------------
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def lift_coefficient(
|
|
421
|
+
wing_surface_area: float,
|
|
422
|
+
aircraft_mass: ArrayOrFloat,
|
|
423
|
+
air_pressure: ArrayOrFloat,
|
|
424
|
+
mach_num: ArrayOrFloat,
|
|
425
|
+
climb_angle: ArrayOrFloat,
|
|
426
|
+
) -> ArrayOrFloat:
|
|
427
|
+
r"""Calculate the lift coefficient.
|
|
428
|
+
|
|
429
|
+
This quantity is a dimensionless coefficient that relates the lift generated
|
|
430
|
+
by a lifting body to the fluid density around the body, the fluid velocity,
|
|
431
|
+
and an associated reference area.
|
|
432
|
+
|
|
433
|
+
Parameters
|
|
434
|
+
----------
|
|
435
|
+
wing_surface_area : float
|
|
436
|
+
Aircraft wing surface area, [:math:`m^2`]
|
|
437
|
+
aircraft_mass : ArrayOrFloat
|
|
438
|
+
Aircraft mass, [:math:`kg`]
|
|
439
|
+
air_pressure: ArrayOrFloat
|
|
440
|
+
Ambient pressure, [:math:`Pa`]
|
|
441
|
+
mach_num : ArrayOrFloat
|
|
442
|
+
Mach number at each waypoint
|
|
443
|
+
climb_angle : ArrayOrFloat
|
|
444
|
+
Angle between the horizontal plane and the actual flight path, [:math:`\deg`]
|
|
445
|
+
|
|
446
|
+
Returns
|
|
447
|
+
-------
|
|
448
|
+
ArrayOrFloat
|
|
449
|
+
Lift coefficient
|
|
450
|
+
|
|
451
|
+
Notes
|
|
452
|
+
-----
|
|
453
|
+
The lift force is perpendicular to the flight direction, while the
|
|
454
|
+
aircraft weight acts vertically.
|
|
455
|
+
|
|
456
|
+
References
|
|
457
|
+
----------
|
|
458
|
+
Eq. (5) of :cite:`pollEstimationMethodFuel2021`.
|
|
459
|
+
"""
|
|
460
|
+
lift_force = aircraft_mass * constants.g * np.cos(units.degrees_to_radians(climb_angle))
|
|
461
|
+
denom = (constants.kappa / 2.0) * air_pressure * mach_num**2 * wing_surface_area
|
|
462
|
+
return lift_force / denom
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def skin_friction_coefficient(rn: ArrayOrFloat) -> ArrayOrFloat:
|
|
466
|
+
"""Calculate aircraft skin friction coefficient.
|
|
467
|
+
|
|
468
|
+
Parameters
|
|
469
|
+
----------
|
|
470
|
+
rn: ArrayOrFloat
|
|
471
|
+
Reynolds number
|
|
472
|
+
|
|
473
|
+
Returns
|
|
474
|
+
-------
|
|
475
|
+
ArrayOrFloat
|
|
476
|
+
Skin friction coefficient.
|
|
477
|
+
|
|
478
|
+
Notes
|
|
479
|
+
-----
|
|
480
|
+
The skin friction coefficient, a dimensionless quantity, is used to estimate the
|
|
481
|
+
skin friction drag, which is the resistance force exerted on an object moving in
|
|
482
|
+
a fluid. Given that aircraft at cruise generally experience a narrow range of
|
|
483
|
+
Reynolds number of between 3E7 and 3E8, it is approximated using a simple power law.
|
|
484
|
+
|
|
485
|
+
References
|
|
486
|
+
----------
|
|
487
|
+
Eq. (28) of :cite:`pollEstimationMethodFuel2021`.
|
|
488
|
+
"""
|
|
489
|
+
return 0.0269 / (rn**0.14)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def zero_lift_drag_coefficient(c_f: ArrayOrFloat, psi_0: float) -> ArrayOrFloat:
|
|
493
|
+
"""Calculate zero-lift drag coefficient.
|
|
494
|
+
|
|
495
|
+
Parameters
|
|
496
|
+
----------
|
|
497
|
+
c_f : ArrayOrFloat
|
|
498
|
+
Skin friction coefficient
|
|
499
|
+
psi_0 : float
|
|
500
|
+
Aircraft geometry drag parameter
|
|
501
|
+
|
|
502
|
+
Returns
|
|
503
|
+
-------
|
|
504
|
+
ArrayOrFloat
|
|
505
|
+
Zero-lift drag coefficient (c_d_0)
|
|
506
|
+
|
|
507
|
+
References
|
|
508
|
+
----------
|
|
509
|
+
Eq. (9) of :cite:`pollEstimationMethodFuel2021`.
|
|
510
|
+
"""
|
|
511
|
+
return c_f * psi_0
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def oswald_efficiency_factor(
|
|
515
|
+
c_drag_0: ArrayOrFloat, atyp_param: PSAircraftEngineParams
|
|
516
|
+
) -> ArrayOrFloat:
|
|
517
|
+
"""Calculate Oswald efficiency factor.
|
|
518
|
+
|
|
519
|
+
The Oswald efficiency factor captures all the lift-dependent drag effects, including
|
|
520
|
+
the vortex drag on the wing (primary source), tailplane and fuselage, and is a function
|
|
521
|
+
of aircraft geometry and the zero-lift drag coefficient.
|
|
522
|
+
|
|
523
|
+
Parameters
|
|
524
|
+
----------
|
|
525
|
+
c_drag_0 : ArrayOrFloat
|
|
526
|
+
Zero-lift drag coefficient.
|
|
527
|
+
atyp_param : PSAircraftEngineParams
|
|
528
|
+
Extracted aircraft and engine parameters.
|
|
529
|
+
|
|
530
|
+
Returns
|
|
531
|
+
-------
|
|
532
|
+
ArrayOrFloat
|
|
533
|
+
Oswald efficiency factor (e_ls)
|
|
534
|
+
|
|
535
|
+
References
|
|
536
|
+
----------
|
|
537
|
+
Eq. (12) of :cite:`pollEstimationMethodFuel2021`.
|
|
538
|
+
"""
|
|
539
|
+
numer = 1.075 if atyp_param.winglets else 1.0
|
|
540
|
+
k_1 = _non_vortex_lift_dependent_drag_factor(c_drag_0, atyp_param.cos_sweep)
|
|
541
|
+
denom = 1.0 + 0.03 + atyp_param.delta_2 + (k_1 * np.pi * atyp_param.wing_aspect_ratio)
|
|
542
|
+
return numer / denom
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _non_vortex_lift_dependent_drag_factor(
|
|
546
|
+
c_drag_0: ArrayOrFloat, cos_sweep: float
|
|
547
|
+
) -> ArrayOrFloat:
|
|
548
|
+
"""Calculate non-vortex lift-dependent drag factor.
|
|
549
|
+
|
|
550
|
+
Parameters
|
|
551
|
+
----------
|
|
552
|
+
c_drag_0 : ArrayOrFloat
|
|
553
|
+
Zero-lift drag coefficient
|
|
554
|
+
cos_sweep : float
|
|
555
|
+
Cosine of wing sweep angle measured at the 1/4 chord line
|
|
556
|
+
|
|
557
|
+
Returns
|
|
558
|
+
-------
|
|
559
|
+
ArrayOrFloat
|
|
560
|
+
Miscellaneous lift-dependent drag factor (k_1)
|
|
561
|
+
|
|
562
|
+
References
|
|
563
|
+
----------
|
|
564
|
+
Eq. (13) of :cite:`pollEstimationMethodFuel2021a`.
|
|
565
|
+
"""
|
|
566
|
+
return 0.8 * (1.0 - 0.53 * cos_sweep) * c_drag_0
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def wave_drag_coefficient(
|
|
570
|
+
mach_num: ArrayOrFloat,
|
|
571
|
+
c_lift: ArrayOrFloat,
|
|
572
|
+
atyp_param: PSAircraftEngineParams,
|
|
573
|
+
) -> ArrayOrFloat:
|
|
574
|
+
"""Calculate wave drag coefficient.
|
|
575
|
+
|
|
576
|
+
Parameters
|
|
577
|
+
----------
|
|
578
|
+
mach_num : ArrayOrFloat
|
|
579
|
+
Mach number at each waypoint
|
|
580
|
+
c_lift : ArrayOrFloat
|
|
581
|
+
Zero-lift drag coefficient
|
|
582
|
+
atyp_param : PSAircraftEngineParams
|
|
583
|
+
Extracted aircraft and engine parameters.
|
|
584
|
+
|
|
585
|
+
Returns
|
|
586
|
+
-------
|
|
587
|
+
ArrayOrFloat
|
|
588
|
+
Wave drag coefficient (c_d_w)
|
|
589
|
+
|
|
590
|
+
Notes
|
|
591
|
+
-----
|
|
592
|
+
The wave drag coefficient captures all the drag resulting from compressibility,
|
|
593
|
+
the development of significant regions of supersonic flow at the wing surface,
|
|
594
|
+
and the formation of shock waves.
|
|
595
|
+
"""
|
|
596
|
+
m_cc = atyp_param.wing_constant - 0.10 * (c_lift / atyp_param.cos_sweep**2)
|
|
597
|
+
x = mach_num * atyp_param.cos_sweep / m_cc
|
|
598
|
+
|
|
599
|
+
c_d_w = np.where(
|
|
600
|
+
x < atyp_param.j_2,
|
|
601
|
+
0.0,
|
|
602
|
+
atyp_param.cos_sweep**3 * atyp_param.j_1 * (x - atyp_param.j_2) ** 2,
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
return np.where( # type: ignore[return-value]
|
|
606
|
+
x < atyp_param.x_ref, c_d_w, c_d_w + atyp_param.j_3 * (x - atyp_param.x_ref) ** 4
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def airframe_drag_coefficient(
|
|
611
|
+
c_drag_0: ArrayOrFloat,
|
|
612
|
+
c_drag_w: ArrayOrFloat,
|
|
613
|
+
c_lift: ArrayOrFloat,
|
|
614
|
+
e_ls: ArrayOrFloat,
|
|
615
|
+
wing_aspect_ratio: float,
|
|
616
|
+
) -> ArrayOrFloat:
|
|
617
|
+
"""Calculate total airframe drag coefficient.
|
|
618
|
+
|
|
619
|
+
Parameters
|
|
620
|
+
----------
|
|
621
|
+
c_drag_0 : ArrayOrFloat
|
|
622
|
+
Zero-lift drag coefficient
|
|
623
|
+
c_drag_w : ArrayOrFloat
|
|
624
|
+
Wave drag coefficient
|
|
625
|
+
c_lift : ArrayOrFloat
|
|
626
|
+
Lift coefficient
|
|
627
|
+
e_ls : ArrayOrFloat
|
|
628
|
+
Oswald efficiency factor
|
|
629
|
+
wing_aspect_ratio : float
|
|
630
|
+
Wing aspect ratio
|
|
631
|
+
|
|
632
|
+
Returns
|
|
633
|
+
-------
|
|
634
|
+
ArrayOrFloat
|
|
635
|
+
Total airframe drag coefficient
|
|
636
|
+
|
|
637
|
+
References
|
|
638
|
+
----------
|
|
639
|
+
Eq. (8) of :cite:`pollEstimationMethodFuel2021`.
|
|
640
|
+
"""
|
|
641
|
+
k = _low_speed_lift_dependent_drag_factor(e_ls, wing_aspect_ratio)
|
|
642
|
+
return c_drag_0 + (k * c_lift**2) + c_drag_w
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _low_speed_lift_dependent_drag_factor(
|
|
646
|
+
e_ls: ArrayOrFloat, wing_aspect_ratio: float
|
|
647
|
+
) -> ArrayOrFloat:
|
|
648
|
+
"""Calculate low-speed lift-dependent drag factor.
|
|
649
|
+
|
|
650
|
+
Parameters
|
|
651
|
+
----------
|
|
652
|
+
e_ls : ArrayOrFloat
|
|
653
|
+
Oswald efficiency factor
|
|
654
|
+
wing_aspect_ratio : float
|
|
655
|
+
Wing aspect ratio
|
|
656
|
+
|
|
657
|
+
Returns
|
|
658
|
+
-------
|
|
659
|
+
ArrayOrFloat
|
|
660
|
+
Low-speed lift-dependent drag factor, K term used to calculate the total
|
|
661
|
+
airframe drag coefficient.
|
|
662
|
+
"""
|
|
663
|
+
return 1.0 / (np.pi * wing_aspect_ratio * e_ls)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
# -------------------
|
|
667
|
+
# Engine parameters
|
|
668
|
+
# -------------------
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def thrust_force(
|
|
672
|
+
aircraft_mass: ArrayOrFloat,
|
|
673
|
+
c_l: ArrayOrFloat,
|
|
674
|
+
c_d: ArrayOrFloat,
|
|
675
|
+
dv_dt: ArrayOrFloat,
|
|
676
|
+
theta: ArrayOrFloat,
|
|
677
|
+
) -> ArrayOrFloat:
|
|
678
|
+
r"""Calculate thrust force summed over all engines.
|
|
679
|
+
|
|
680
|
+
Parameters
|
|
681
|
+
----------
|
|
682
|
+
aircraft_mass : ArrayOrFloat
|
|
683
|
+
Aircraft mass at each waypoint, [:math:`kg`]
|
|
684
|
+
c_l : ArrayOrFloat
|
|
685
|
+
Lift coefficient
|
|
686
|
+
c_d : ArrayOrFloat
|
|
687
|
+
Total airframe drag coefficient
|
|
688
|
+
dv_dt : ArrayOrFloat
|
|
689
|
+
Acceleration/deceleration at each waypoint, [:math:`m \ s^{-2}`]
|
|
690
|
+
theta : ArrayOrFloat
|
|
691
|
+
Climb (positive value) or descent (negative value) angle, [:math:`\deg`]
|
|
692
|
+
|
|
693
|
+
Returns
|
|
694
|
+
-------
|
|
695
|
+
ArrayOrFloat
|
|
696
|
+
Thrust force summed over all engines, [:math:`N`]
|
|
697
|
+
|
|
698
|
+
Notes
|
|
699
|
+
-----
|
|
700
|
+
- The lift-to-drag ratio is calculated using ``c_l`` and ``c_d``,
|
|
701
|
+
- The first term ``(mg * cos(theta) * (D/L))`` represents the drag force,
|
|
702
|
+
- The second term ``(mg * sin(theta))`` represents the aircraft weight acting on
|
|
703
|
+
the flight direction,
|
|
704
|
+
- The third term ``(m * a)`` represents the force required to accelerate the aircraft.
|
|
705
|
+
|
|
706
|
+
References
|
|
707
|
+
----------
|
|
708
|
+
Eq. (95) of :cite:`pollEstimationMethodFuel2021a`.
|
|
709
|
+
"""
|
|
710
|
+
theta = units.degrees_to_radians(theta)
|
|
711
|
+
f_thrust = (
|
|
712
|
+
(aircraft_mass * constants.g * np.cos(theta) * (c_d / c_l))
|
|
713
|
+
+ (aircraft_mass * constants.g * np.sin(theta))
|
|
714
|
+
+ aircraft_mass * dv_dt
|
|
715
|
+
)
|
|
716
|
+
return f_thrust.clip(min=0.0)
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def engine_thrust_coefficient(
|
|
720
|
+
f_thrust: ArrayOrFloat,
|
|
721
|
+
mach_num: ArrayOrFloat,
|
|
722
|
+
air_pressure: ArrayOrFloat,
|
|
723
|
+
wing_surface_area: float,
|
|
724
|
+
) -> ArrayOrFloat:
|
|
725
|
+
"""Calculate engine thrust coefficient.
|
|
726
|
+
|
|
727
|
+
Parameters
|
|
728
|
+
----------
|
|
729
|
+
f_thrust : ArrayOrFloat
|
|
730
|
+
Thrust force summed over all engines, [:math:`N`]
|
|
731
|
+
mach_num : ArrayOrFloat
|
|
732
|
+
Mach number at each waypoint
|
|
733
|
+
air_pressure : ArrayOrFloat
|
|
734
|
+
Ambient pressure, [:math:`Pa`]
|
|
735
|
+
wing_surface_area : float
|
|
736
|
+
Aircraft wing surface area, [:math:`m^2`]
|
|
737
|
+
|
|
738
|
+
Returns
|
|
739
|
+
-------
|
|
740
|
+
ArrayOrFloat
|
|
741
|
+
Engine thrust coefficient (c_t)
|
|
742
|
+
"""
|
|
743
|
+
return f_thrust / (0.5 * constants.kappa * air_pressure * mach_num**2 * wing_surface_area)
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def overall_propulsion_efficiency(
|
|
747
|
+
mach_num: ArrayOrFloat,
|
|
748
|
+
c_t: ArrayOrFloat,
|
|
749
|
+
c_t_eta_b: ArrayOrFloat,
|
|
750
|
+
atyp_param: PSAircraftEngineParams,
|
|
751
|
+
eta_over_eta_b_min: float | None = None,
|
|
752
|
+
) -> npt.NDArray[np.floating]:
|
|
753
|
+
"""Calculate overall propulsion efficiency.
|
|
754
|
+
|
|
755
|
+
Parameters
|
|
756
|
+
----------
|
|
757
|
+
mach_num : ArrayOrFloat
|
|
758
|
+
Mach number at each waypoint
|
|
759
|
+
c_t : ArrayOrFloat
|
|
760
|
+
Engine thrust coefficient
|
|
761
|
+
c_t_eta_b : ArrayOrFloat
|
|
762
|
+
Thrust coefficient at maximum overall propulsion efficiency for a given Mach Number.
|
|
763
|
+
atyp_param : PSAircraftEngineParams
|
|
764
|
+
Extracted aircraft and engine parameters.
|
|
765
|
+
eta_over_eta_b_min : float | None, optional
|
|
766
|
+
Clip the ratio of the overall propulsion efficiency to the maximum propulsion
|
|
767
|
+
efficiency to this value. See :func:`propulsion_efficiency_over_max_propulsion_efficiency`.
|
|
768
|
+
If ``None``, no clipping is performed.
|
|
769
|
+
|
|
770
|
+
Returns
|
|
771
|
+
-------
|
|
772
|
+
npt.NDArray[np.floating]
|
|
773
|
+
Overall propulsion efficiency
|
|
774
|
+
"""
|
|
775
|
+
eta_over_eta_b = propulsion_efficiency_over_max_propulsion_efficiency(mach_num, c_t, c_t_eta_b)
|
|
776
|
+
if eta_over_eta_b_min is not None:
|
|
777
|
+
eta_over_eta_b.clip(min=eta_over_eta_b_min, out=eta_over_eta_b)
|
|
778
|
+
eta_b = max_overall_propulsion_efficiency(
|
|
779
|
+
mach_num, atyp_param.m_des, atyp_param.eta_1, atyp_param.eta_2
|
|
780
|
+
)
|
|
781
|
+
return eta_over_eta_b * eta_b
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def propulsion_efficiency_over_max_propulsion_efficiency(
|
|
785
|
+
mach_num: ArrayOrFloat,
|
|
786
|
+
c_t: ArrayOrFloat,
|
|
787
|
+
c_t_eta_b: ArrayOrFloat,
|
|
788
|
+
) -> npt.NDArray[np.floating]:
|
|
789
|
+
"""Calculate ratio of OPE to maximum OPE that can be attained for a given Mach number.
|
|
790
|
+
|
|
791
|
+
Parameters
|
|
792
|
+
----------
|
|
793
|
+
mach_num : ArrayOrFloat
|
|
794
|
+
Mach number at each waypoint.
|
|
795
|
+
c_t : ArrayOrFloat
|
|
796
|
+
Engine thrust coefficient.
|
|
797
|
+
c_t_eta_b : ArrayOrFloat
|
|
798
|
+
Thrust coefficient at maximum overall propulsion efficiency for a given Mach Number.
|
|
799
|
+
|
|
800
|
+
Returns
|
|
801
|
+
-------
|
|
802
|
+
npt.NDArray[np.floating]
|
|
803
|
+
Ratio of OPE to maximum OPE, ``eta / eta_b``
|
|
804
|
+
|
|
805
|
+
Notes
|
|
806
|
+
-----
|
|
807
|
+
- ``eta / eta_b`` is approximated using a fourth-order polynomial
|
|
808
|
+
- ``eta_b`` is the maximum overall propulsion efficiency for a given Mach number
|
|
809
|
+
"""
|
|
810
|
+
c_t_over_c_t_eta_b = c_t / c_t_eta_b
|
|
811
|
+
|
|
812
|
+
sigma = np.where(mach_num < 0.4, 1.3 * (0.4 - mach_num), np.float32(0.0)) # avoid promotion
|
|
813
|
+
|
|
814
|
+
eta_over_eta_b_low = (
|
|
815
|
+
10.0 * (1.0 + 0.8 * (sigma - 0.43) - 0.6027 * sigma * 0.43) * c_t_over_c_t_eta_b
|
|
816
|
+
+ 33.3333 * (-1.0 - 0.97 * (sigma - 0.43) + 0.8281 * sigma * 0.43) * (c_t_over_c_t_eta_b**2)
|
|
817
|
+
+ 37.037 * (1.0 + (sigma - 0.43) - 0.9163 * sigma * 0.43) * (c_t_over_c_t_eta_b**3)
|
|
818
|
+
)
|
|
819
|
+
eta_over_eta_b_hi = (
|
|
820
|
+
(1.0 + (sigma - 0.43) - sigma * 0.43)
|
|
821
|
+
+ (4.0 * sigma * 0.43 - 2.0 * (sigma - 0.43)) * c_t_over_c_t_eta_b
|
|
822
|
+
+ ((sigma - 0.43) - 6 * sigma * 0.43) * (c_t_over_c_t_eta_b**2)
|
|
823
|
+
+ 4.0 * sigma * 0.43 * (c_t_over_c_t_eta_b**3)
|
|
824
|
+
- sigma * 0.43 * (c_t_over_c_t_eta_b**4)
|
|
825
|
+
)
|
|
826
|
+
return np.where(c_t_over_c_t_eta_b < 0.3, eta_over_eta_b_low, eta_over_eta_b_hi)
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def thrust_coefficient_at_max_efficiency(
|
|
830
|
+
mach_num: ArrayOrFloat, m_des: float, c_t_des: float
|
|
831
|
+
) -> ArrayOrFloat:
|
|
832
|
+
"""
|
|
833
|
+
Calculate thrust coefficient at maximum overall propulsion efficiency for a given Mach Number.
|
|
834
|
+
|
|
835
|
+
Parameters
|
|
836
|
+
----------
|
|
837
|
+
mach_num : ArrayOrFloat
|
|
838
|
+
Mach number at each waypoint.
|
|
839
|
+
m_des: float
|
|
840
|
+
Design optimum Mach number where the fuel mass flow rate is at a minimum.
|
|
841
|
+
c_t_des: float
|
|
842
|
+
Design optimum engine thrust coefficient where the fuel mass flow rate is at a minimum.
|
|
843
|
+
|
|
844
|
+
Returns
|
|
845
|
+
-------
|
|
846
|
+
ArrayOrFloat
|
|
847
|
+
Thrust coefficient at maximum overall propulsion efficiency for a given
|
|
848
|
+
Mach Number, ``(c_t)_eta_b``
|
|
849
|
+
"""
|
|
850
|
+
m_over_m_des = mach_num / m_des
|
|
851
|
+
h_2 = ((1.0 + 0.55 * mach_num) / (1.0 + 0.55 * m_des)) / (m_over_m_des**2)
|
|
852
|
+
return h_2 * c_t_des
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def max_overall_propulsion_efficiency(
|
|
856
|
+
mach_num: ArrayOrFloat, mach_num_des: float, eta_1: float, eta_2: float
|
|
857
|
+
) -> ArrayOrFloat:
|
|
858
|
+
"""
|
|
859
|
+
Calculate maximum overall propulsion efficiency that can be achieved for a given Mach number.
|
|
860
|
+
|
|
861
|
+
Parameters
|
|
862
|
+
----------
|
|
863
|
+
mach_num : ArrayOrFloat
|
|
864
|
+
Mach number at each waypoint
|
|
865
|
+
mach_num_des : float
|
|
866
|
+
Design optimum Mach number where the fuel mass flow rate is at a minimum.
|
|
867
|
+
eta_1 : float
|
|
868
|
+
Multiplier for maximum overall propulsion efficiency model, varies by aircraft type
|
|
869
|
+
eta_2 : float
|
|
870
|
+
Exponent for maximum overall propulsion efficiency model, varies by aircraft type
|
|
871
|
+
|
|
872
|
+
Returns
|
|
873
|
+
-------
|
|
874
|
+
ArrayOrFloat
|
|
875
|
+
Maximum overall propulsion efficiency that can be achieved for a given Mach number
|
|
876
|
+
|
|
877
|
+
References
|
|
878
|
+
----------
|
|
879
|
+
Eq. (35) of :cite:`pollEstimationMethodFuel2021a`.
|
|
880
|
+
"""
|
|
881
|
+
# XXX: h_1 may be varied in the future
|
|
882
|
+
# The current implementation looks like:
|
|
883
|
+
# h_1 = (mach_num / mach_num_des) ** eta_2
|
|
884
|
+
# return h_1 * eta_1 * mach_num_des**eta_2
|
|
885
|
+
|
|
886
|
+
return eta_1 * mach_num**eta_2
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
# -------------------
|
|
890
|
+
# Fuel consumption
|
|
891
|
+
# -------------------
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def fuel_mass_flow_rate(
|
|
895
|
+
air_pressure: ArrayOrFloat,
|
|
896
|
+
air_temperature: ArrayOrFloat,
|
|
897
|
+
mach_num: ArrayOrFloat,
|
|
898
|
+
c_t: ArrayOrFloat,
|
|
899
|
+
eta: ArrayOrFloat | float,
|
|
900
|
+
wing_surface_area: float,
|
|
901
|
+
q_fuel: float,
|
|
902
|
+
) -> ArrayOrFloat:
|
|
903
|
+
r"""Calculate fuel mass flow rate.
|
|
904
|
+
|
|
905
|
+
Parameters
|
|
906
|
+
----------
|
|
907
|
+
air_pressure : ArrayOrFloat
|
|
908
|
+
Ambient pressure, [:math:`Pa`]
|
|
909
|
+
air_temperature : ArrayOrFloat
|
|
910
|
+
Ambient temperature at each waypoint, [:math:`K`]
|
|
911
|
+
mach_num : ArrayOrFloat
|
|
912
|
+
Mach number at each waypoint
|
|
913
|
+
c_t : ArrayOrFloat
|
|
914
|
+
Engine thrust coefficient
|
|
915
|
+
eta : ArrayOrFloat | float
|
|
916
|
+
Overall propulsion efficiency
|
|
917
|
+
wing_surface_area : float
|
|
918
|
+
Aircraft wing surface area, [:math:`m^2`]
|
|
919
|
+
q_fuel : float
|
|
920
|
+
Lower calorific value (LCV) of fuel, [:math:`J \ kg_{fuel}^{-1}`]
|
|
921
|
+
|
|
922
|
+
Returns
|
|
923
|
+
-------
|
|
924
|
+
ArrayOrFloat
|
|
925
|
+
Fuel mass flow rate, [:math:`kg s^{-1}`]
|
|
926
|
+
"""
|
|
927
|
+
return (
|
|
928
|
+
(constants.kappa / 2)
|
|
929
|
+
* (c_t * mach_num**3 / eta)
|
|
930
|
+
* (constants.kappa * constants.R_d * air_temperature) ** 0.5
|
|
931
|
+
* air_pressure
|
|
932
|
+
* wing_surface_area
|
|
933
|
+
/ q_fuel
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def fuel_flow_correction(
|
|
938
|
+
fuel_flow: ArrayOrFloat,
|
|
939
|
+
altitude_ft: ArrayOrFloat,
|
|
940
|
+
air_temperature: ArrayOrFloat,
|
|
941
|
+
air_pressure: ArrayOrFloat,
|
|
942
|
+
mach_number: ArrayOrFloat,
|
|
943
|
+
fuel_flow_idle_sls: float,
|
|
944
|
+
fuel_flow_max_sls: float,
|
|
945
|
+
flight_phase: npt.NDArray[np.uint8] | flight.FlightPhase,
|
|
946
|
+
) -> ArrayOrFloat:
|
|
947
|
+
r"""Correct fuel mass flow rate to ensure that they are within operational limits.
|
|
948
|
+
|
|
949
|
+
Parameters
|
|
950
|
+
----------
|
|
951
|
+
fuel_flow : ArrayOrFloat
|
|
952
|
+
Fuel mass flow rate, [:math:`kg s^{-1}`]
|
|
953
|
+
altitude_ft : ArrayOrFloat
|
|
954
|
+
Waypoint altitude, [:math: `ft`]
|
|
955
|
+
air_temperature : ArrayOrFloat
|
|
956
|
+
Ambient temperature at each waypoint, [:math:`K`]
|
|
957
|
+
air_pressure : ArrayOrFloat
|
|
958
|
+
Ambient pressure, [:math:`Pa`]
|
|
959
|
+
mach_number : ArrayOrFloat
|
|
960
|
+
Mach number
|
|
961
|
+
fuel_flow_idle_sls : float
|
|
962
|
+
Fuel mass flow rate under engine idle and sea level static conditions, [:math:`kg \ s^{-1}`]
|
|
963
|
+
fuel_flow_max_sls : float
|
|
964
|
+
Fuel mass flow rate at take-off and sea level static conditions, [:math:`kg \ s^{-1}`]
|
|
965
|
+
flight_phase : npt.NDArray[np.uint8] | flight.FlightPhase
|
|
966
|
+
Phase state of each waypoint.
|
|
967
|
+
|
|
968
|
+
Returns
|
|
969
|
+
-------
|
|
970
|
+
ArrayOrFloat
|
|
971
|
+
Corrected fuel mass flow rate, [:math:`kg \ s^{-1}`]
|
|
972
|
+
"""
|
|
973
|
+
ff_min = ps_lims.fuel_flow_idle(fuel_flow_idle_sls, altitude_ft)
|
|
974
|
+
ff_max = jet.equivalent_fuel_flow_rate_at_cruise(
|
|
975
|
+
fuel_flow_max_sls,
|
|
976
|
+
(air_temperature / constants.T_msl),
|
|
977
|
+
(air_pressure / constants.p_surface),
|
|
978
|
+
mach_number,
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
# Account for descent conditions
|
|
982
|
+
# Assume max_fuel_flow at descent is not more than 30% of fuel_flow_max_sls
|
|
983
|
+
# We need this assumption because PTF files are not available in the PS model.
|
|
984
|
+
descent = flight_phase == flight.FlightPhase.DESCENT
|
|
985
|
+
ff_max[descent] = 0.3 * fuel_flow_max_sls
|
|
986
|
+
return np.clip(fuel_flow, ff_min, ff_max)
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
@functools.cache
|
|
990
|
+
def get_aircraft_synonym_dict_ps() -> dict[str, str]:
|
|
991
|
+
"""Read `ps-synonym-list-20240524.csv` from the static directory.
|
|
992
|
+
|
|
993
|
+
Returns
|
|
994
|
+
-------
|
|
995
|
+
dict[str, str]
|
|
996
|
+
Dictionary of the form ``{"icao_aircraft_type": "ps_aircraft_type"}``.
|
|
997
|
+
"""
|
|
998
|
+
# get path to static PS synonym list
|
|
999
|
+
df_atyp_icao_to_ps = pd.read_csv(PS_SYNONYM_FILE_PATH, index_col="ICAO Aircraft Code")
|
|
1000
|
+
return df_atyp_icao_to_ps["PS ATYP"].to_dict()
|