pycontrails 0.59.0__cp314-cp314-macosx_10_15_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 (123) hide show
  1. pycontrails/__init__.py +70 -0
  2. pycontrails/_version.py +34 -0
  3. pycontrails/core/__init__.py +30 -0
  4. pycontrails/core/aircraft_performance.py +679 -0
  5. pycontrails/core/airports.py +228 -0
  6. pycontrails/core/cache.py +889 -0
  7. pycontrails/core/coordinates.py +174 -0
  8. pycontrails/core/fleet.py +483 -0
  9. pycontrails/core/flight.py +2185 -0
  10. pycontrails/core/flightplan.py +228 -0
  11. pycontrails/core/fuel.py +140 -0
  12. pycontrails/core/interpolation.py +702 -0
  13. pycontrails/core/met.py +2936 -0
  14. pycontrails/core/met_var.py +387 -0
  15. pycontrails/core/models.py +1321 -0
  16. pycontrails/core/polygon.py +549 -0
  17. pycontrails/core/rgi_cython.cpython-314-darwin.so +0 -0
  18. pycontrails/core/vector.py +2249 -0
  19. pycontrails/datalib/__init__.py +12 -0
  20. pycontrails/datalib/_met_utils/metsource.py +746 -0
  21. pycontrails/datalib/ecmwf/__init__.py +73 -0
  22. pycontrails/datalib/ecmwf/arco_era5.py +345 -0
  23. pycontrails/datalib/ecmwf/common.py +114 -0
  24. pycontrails/datalib/ecmwf/era5.py +554 -0
  25. pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
  26. pycontrails/datalib/ecmwf/hres.py +804 -0
  27. pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
  28. pycontrails/datalib/ecmwf/ifs.py +287 -0
  29. pycontrails/datalib/ecmwf/model_levels.py +435 -0
  30. pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
  31. pycontrails/datalib/ecmwf/variables.py +268 -0
  32. pycontrails/datalib/geo_utils.py +261 -0
  33. pycontrails/datalib/gfs/__init__.py +28 -0
  34. pycontrails/datalib/gfs/gfs.py +656 -0
  35. pycontrails/datalib/gfs/variables.py +104 -0
  36. pycontrails/datalib/goes.py +764 -0
  37. pycontrails/datalib/gruan.py +343 -0
  38. pycontrails/datalib/himawari/__init__.py +27 -0
  39. pycontrails/datalib/himawari/header_struct.py +266 -0
  40. pycontrails/datalib/himawari/himawari.py +671 -0
  41. pycontrails/datalib/landsat.py +589 -0
  42. pycontrails/datalib/leo_utils/__init__.py +5 -0
  43. pycontrails/datalib/leo_utils/correction.py +266 -0
  44. pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
  45. pycontrails/datalib/leo_utils/search.py +250 -0
  46. pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
  47. pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
  48. pycontrails/datalib/leo_utils/vis.py +59 -0
  49. pycontrails/datalib/sentinel.py +650 -0
  50. pycontrails/datalib/spire/__init__.py +5 -0
  51. pycontrails/datalib/spire/exceptions.py +62 -0
  52. pycontrails/datalib/spire/spire.py +604 -0
  53. pycontrails/ext/bada.py +42 -0
  54. pycontrails/ext/cirium.py +14 -0
  55. pycontrails/ext/empirical_grid.py +140 -0
  56. pycontrails/ext/synthetic_flight.py +431 -0
  57. pycontrails/models/__init__.py +1 -0
  58. pycontrails/models/accf.py +425 -0
  59. pycontrails/models/apcemm/__init__.py +8 -0
  60. pycontrails/models/apcemm/apcemm.py +983 -0
  61. pycontrails/models/apcemm/inputs.py +226 -0
  62. pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
  63. pycontrails/models/apcemm/utils.py +437 -0
  64. pycontrails/models/cocip/__init__.py +29 -0
  65. pycontrails/models/cocip/cocip.py +2742 -0
  66. pycontrails/models/cocip/cocip_params.py +305 -0
  67. pycontrails/models/cocip/cocip_uncertainty.py +291 -0
  68. pycontrails/models/cocip/contrail_properties.py +1530 -0
  69. pycontrails/models/cocip/output_formats.py +2270 -0
  70. pycontrails/models/cocip/radiative_forcing.py +1260 -0
  71. pycontrails/models/cocip/radiative_heating.py +520 -0
  72. pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
  73. pycontrails/models/cocip/wake_vortex.py +396 -0
  74. pycontrails/models/cocip/wind_shear.py +120 -0
  75. pycontrails/models/cocipgrid/__init__.py +9 -0
  76. pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
  77. pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
  78. pycontrails/models/dry_advection.py +602 -0
  79. pycontrails/models/emissions/__init__.py +21 -0
  80. pycontrails/models/emissions/black_carbon.py +599 -0
  81. pycontrails/models/emissions/emissions.py +1353 -0
  82. pycontrails/models/emissions/ffm2.py +336 -0
  83. pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
  84. pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
  85. pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
  86. pycontrails/models/extended_k15.py +1327 -0
  87. pycontrails/models/humidity_scaling/__init__.py +37 -0
  88. pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
  89. pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
  90. pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
  91. pycontrails/models/issr.py +210 -0
  92. pycontrails/models/pcc.py +326 -0
  93. pycontrails/models/pcr.py +154 -0
  94. pycontrails/models/ps_model/__init__.py +18 -0
  95. pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
  96. pycontrails/models/ps_model/ps_grid.py +701 -0
  97. pycontrails/models/ps_model/ps_model.py +1000 -0
  98. pycontrails/models/ps_model/ps_operational_limits.py +525 -0
  99. pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
  100. pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
  101. pycontrails/models/sac.py +442 -0
  102. pycontrails/models/tau_cirrus.py +183 -0
  103. pycontrails/physics/__init__.py +1 -0
  104. pycontrails/physics/constants.py +117 -0
  105. pycontrails/physics/geo.py +1138 -0
  106. pycontrails/physics/jet.py +968 -0
  107. pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
  108. pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
  109. pycontrails/physics/thermo.py +551 -0
  110. pycontrails/physics/units.py +472 -0
  111. pycontrails/py.typed +0 -0
  112. pycontrails/utils/__init__.py +1 -0
  113. pycontrails/utils/dependencies.py +66 -0
  114. pycontrails/utils/iteration.py +13 -0
  115. pycontrails/utils/json.py +187 -0
  116. pycontrails/utils/temp.py +50 -0
  117. pycontrails/utils/types.py +163 -0
  118. pycontrails-0.59.0.dist-info/METADATA +179 -0
  119. pycontrails-0.59.0.dist-info/RECORD +123 -0
  120. pycontrails-0.59.0.dist-info/WHEEL +6 -0
  121. pycontrails-0.59.0.dist-info/licenses/LICENSE +178 -0
  122. pycontrails-0.59.0.dist-info/licenses/NOTICE +43 -0
  123. pycontrails-0.59.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()