pycontrails 0.53.0__cp313-cp313-manylinux_2_17_x86_64.manylinux2014_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.

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