pycontrails 0.58.0__cp314-cp314-macosx_10_13_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pycontrails might be problematic. Click here for more details.

Files changed (122) 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 +2931 -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 +757 -0
  37. pycontrails/datalib/himawari/__init__.py +27 -0
  38. pycontrails/datalib/himawari/header_struct.py +266 -0
  39. pycontrails/datalib/himawari/himawari.py +667 -0
  40. pycontrails/datalib/landsat.py +589 -0
  41. pycontrails/datalib/leo_utils/__init__.py +5 -0
  42. pycontrails/datalib/leo_utils/correction.py +266 -0
  43. pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
  44. pycontrails/datalib/leo_utils/search.py +250 -0
  45. pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
  46. pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
  47. pycontrails/datalib/leo_utils/vis.py +59 -0
  48. pycontrails/datalib/sentinel.py +650 -0
  49. pycontrails/datalib/spire/__init__.py +5 -0
  50. pycontrails/datalib/spire/exceptions.py +62 -0
  51. pycontrails/datalib/spire/spire.py +604 -0
  52. pycontrails/ext/bada.py +42 -0
  53. pycontrails/ext/cirium.py +14 -0
  54. pycontrails/ext/empirical_grid.py +140 -0
  55. pycontrails/ext/synthetic_flight.py +431 -0
  56. pycontrails/models/__init__.py +1 -0
  57. pycontrails/models/accf.py +425 -0
  58. pycontrails/models/apcemm/__init__.py +8 -0
  59. pycontrails/models/apcemm/apcemm.py +983 -0
  60. pycontrails/models/apcemm/inputs.py +226 -0
  61. pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
  62. pycontrails/models/apcemm/utils.py +437 -0
  63. pycontrails/models/cocip/__init__.py +29 -0
  64. pycontrails/models/cocip/cocip.py +2742 -0
  65. pycontrails/models/cocip/cocip_params.py +305 -0
  66. pycontrails/models/cocip/cocip_uncertainty.py +291 -0
  67. pycontrails/models/cocip/contrail_properties.py +1530 -0
  68. pycontrails/models/cocip/output_formats.py +2270 -0
  69. pycontrails/models/cocip/radiative_forcing.py +1260 -0
  70. pycontrails/models/cocip/radiative_heating.py +520 -0
  71. pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
  72. pycontrails/models/cocip/wake_vortex.py +396 -0
  73. pycontrails/models/cocip/wind_shear.py +120 -0
  74. pycontrails/models/cocipgrid/__init__.py +9 -0
  75. pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
  76. pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
  77. pycontrails/models/dry_advection.py +602 -0
  78. pycontrails/models/emissions/__init__.py +21 -0
  79. pycontrails/models/emissions/black_carbon.py +599 -0
  80. pycontrails/models/emissions/emissions.py +1353 -0
  81. pycontrails/models/emissions/ffm2.py +336 -0
  82. pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
  83. pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
  84. pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
  85. pycontrails/models/extended_k15.py +1327 -0
  86. pycontrails/models/humidity_scaling/__init__.py +37 -0
  87. pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
  88. pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
  89. pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
  90. pycontrails/models/issr.py +210 -0
  91. pycontrails/models/pcc.py +326 -0
  92. pycontrails/models/pcr.py +154 -0
  93. pycontrails/models/ps_model/__init__.py +18 -0
  94. pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
  95. pycontrails/models/ps_model/ps_grid.py +701 -0
  96. pycontrails/models/ps_model/ps_model.py +1000 -0
  97. pycontrails/models/ps_model/ps_operational_limits.py +525 -0
  98. pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
  99. pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
  100. pycontrails/models/sac.py +442 -0
  101. pycontrails/models/tau_cirrus.py +183 -0
  102. pycontrails/physics/__init__.py +1 -0
  103. pycontrails/physics/constants.py +117 -0
  104. pycontrails/physics/geo.py +1138 -0
  105. pycontrails/physics/jet.py +968 -0
  106. pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
  107. pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
  108. pycontrails/physics/thermo.py +551 -0
  109. pycontrails/physics/units.py +472 -0
  110. pycontrails/py.typed +0 -0
  111. pycontrails/utils/__init__.py +1 -0
  112. pycontrails/utils/dependencies.py +66 -0
  113. pycontrails/utils/iteration.py +13 -0
  114. pycontrails/utils/json.py +187 -0
  115. pycontrails/utils/temp.py +50 -0
  116. pycontrails/utils/types.py +163 -0
  117. pycontrails-0.58.0.dist-info/METADATA +180 -0
  118. pycontrails-0.58.0.dist-info/RECORD +122 -0
  119. pycontrails-0.58.0.dist-info/WHEEL +6 -0
  120. pycontrails-0.58.0.dist-info/licenses/LICENSE +178 -0
  121. pycontrails-0.58.0.dist-info/licenses/NOTICE +43 -0
  122. pycontrails-0.58.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,679 @@
1
+ """Abstract interfaces for aircraft performance models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import abc
6
+ import dataclasses
7
+ import warnings
8
+ from typing import Any, Generic, NoReturn, overload
9
+
10
+ import numpy as np
11
+ import numpy.typing as npt
12
+
13
+ from pycontrails.core import flight, fuel
14
+ from pycontrails.core.fleet import Fleet
15
+ from pycontrails.core.flight import Flight
16
+ from pycontrails.core.met import MetDataset
17
+ from pycontrails.core.met_var import AirTemperature, EastwardWind, MetVariable, NorthwardWind
18
+ from pycontrails.core.models import Model, ModelParams, interpolate_met
19
+ from pycontrails.core.vector import GeoVectorDataset
20
+ from pycontrails.physics import jet
21
+ from pycontrails.utils.types import ArrayOrFloat
22
+
23
+ #: Default load factor for aircraft performance models.
24
+ #: See :func:`pycontrails.physics.jet.aircraft_load_factor`
25
+ #: for a higher precision approach to estimating the load factor.
26
+ DEFAULT_LOAD_FACTOR = 0.83
27
+
28
+
29
+ # --------------------------------------
30
+ # Trajectory aircraft performance models
31
+ # --------------------------------------
32
+
33
+
34
+ @dataclasses.dataclass
35
+ class CommonAircraftPerformanceParams:
36
+ """Params for :class:`AircraftPerformanceParams` and :class:`AircraftPerformanceGridParams`."""
37
+
38
+ #: Account for "in-service" engine deterioration between maintenance cycles.
39
+ #: Default value is set to +2.5% increase in fuel consumption.
40
+ #: Reference:
41
+ #: Gurrola Arrieta, M.D.J., Botez, R.M. and Lasne, A., 2024. An Engine Deterioration Model for
42
+ #: Predicting Fuel Consumption Impact in a Regional Aircraft. Aerospace, 11(6), p.426.
43
+ engine_deterioration_factor: float = 0.025
44
+
45
+
46
+ @dataclasses.dataclass
47
+ class AircraftPerformanceParams(ModelParams, CommonAircraftPerformanceParams):
48
+ """Parameters for :class:`AircraftPerformance`."""
49
+
50
+ #: Whether to correct fuel flow to ensure it remains within
51
+ #: the operational limits of the aircraft type.
52
+ correct_fuel_flow: bool = True
53
+
54
+ #: The number of iterations used to calculate aircraft mass and fuel flow.
55
+ #: The default value of 3 is sufficient for most cases.
56
+ n_iter: int = 3
57
+
58
+ #: Experimental. If True, fill waypoints below the lowest altitude met
59
+ #: level with ISA temperature when interpolating "air_temperature" or "t".
60
+ #: If the ``met`` data is not provided, the entire air temperature array
61
+ #: is approximated with the ISA temperature. Enabling this does NOT
62
+ #: remove any NaN values in the ``met`` data itself.
63
+ fill_low_altitude_with_isa_temperature: bool = False
64
+
65
+ #: Experimental. If True, fill waypoints below the lowest altitude met
66
+ #: level with zero wind when computing true airspeed. In other words,
67
+ #: approximate low-altitude true airspeed with the ground speed. Enabling
68
+ #: this does NOT remove any NaN values in the ``met`` data itself.
69
+ #: In the case that ``met`` is not provided, any missing values are
70
+ #: filled with zero wind.
71
+ fill_low_altitude_with_zero_wind: bool = False
72
+
73
+
74
+ class AircraftPerformance(Model):
75
+ """
76
+ Support for standardizing aircraft performance methodologies.
77
+
78
+ This class provides a :meth:`simulate_fuel_and_performance` method for
79
+ iteratively calculating aircraft mass and fuel flow rate.
80
+
81
+ The implementing class must bring :meth:`eval` and
82
+ :meth:`calculate_aircraft_performance` methods. At runtime, these methods
83
+ are intended to be chained together as follows:
84
+
85
+ 1. The :meth:`eval` method is called with a :class:`Flight`
86
+ 2. The :meth:`simulate_fuel_and_performance` method is called inside :meth:`eval`
87
+ to iteratively calculate aircraft mass and fuel flow rate. If an aircraft
88
+ mass is provided, the fuel flow rate is calculated once directly with a single
89
+ call to :meth:`calculate_aircraft_performance`. If an aircraft mass is not
90
+ provided, the fuel flow rate is calculated iteratively with multiple calls to
91
+ :meth:`calculate_aircraft_performance`.
92
+ """
93
+
94
+ source: Flight
95
+ met_variables: tuple[MetVariable, ...] = ()
96
+ optional_met_variables: tuple[MetVariable, ...] = (AirTemperature, EastwardWind, NorthwardWind)
97
+ default_params = AircraftPerformanceParams
98
+
99
+ @overload
100
+ def eval(self, source: Fleet, **params: Any) -> Fleet: ...
101
+
102
+ @overload
103
+ def eval(self, source: Flight, **params: Any) -> Flight: ...
104
+
105
+ @overload
106
+ def eval(self, source: None = ..., **params: Any) -> NoReturn: ...
107
+
108
+ def eval(self, source: Flight | None = None, **params: Any) -> Flight:
109
+ """Evaluate the aircraft performance model.
110
+
111
+ Parameters
112
+ ----------
113
+ source : Flight
114
+ Flight trajectory to evaluate. Can be a :class:`Flight` or :class:`Fleet`.
115
+ params : Any
116
+ Override :attr:`params` with keyword arguments.
117
+
118
+ Returns
119
+ -------
120
+ Flight
121
+ Flight trajectory with aircraft performance data.
122
+ """
123
+ self.update_params(params)
124
+ self.set_source(source)
125
+ self.source = self.require_source_type(Flight)
126
+ self.downselect_met()
127
+ self.set_source_met()
128
+ self._cleanup_indices()
129
+
130
+ # Calculate temperature and true airspeed if not included on source
131
+ self.ensure_air_temperature_on_source()
132
+ self.ensure_true_airspeed_on_source()
133
+
134
+ if isinstance(self.source, Fleet):
135
+ fls = [self.eval_flight(fl) for fl in self.source.to_flight_list()]
136
+ self.source = Fleet.from_seq(fls, attrs=self.source.attrs, broadcast_numeric=False)
137
+ return self.source
138
+
139
+ self.source = self.eval_flight(self.source)
140
+ return self.source
141
+
142
+ @abc.abstractmethod
143
+ def eval_flight(self, fl: Flight) -> Flight:
144
+ """Evaluate the aircraft performance model on a single flight trajectory.
145
+
146
+ The implementing model adds the following fields to the source flight:
147
+
148
+ - ``aircraft_mass``: aircraft mass at each waypoint, [:math:`kg`]
149
+ - ``fuel_flow``: fuel mass flow rate at each waypoint, [:math:`kg s^{-1}`]
150
+ - ``thrust``: thrust at each waypoint, [:math:`N`]
151
+ - ``engine_efficiency``: engine efficiency at each waypoint
152
+ - ``rocd``: rate of climb or descent at each waypoint, [:math:`ft min^{-1}`]
153
+ - ``fuel_burn``: fuel burn at each waypoint, [:math:`kg`]
154
+
155
+ In addition, the following attributes are added to the source flight:
156
+
157
+ - ``n_engine``: number of engines
158
+ - ``wingspan``: wingspan, [:math:`m`]
159
+ - ``max_mach``: maximum Mach number
160
+ - ``max_altitude``: maximum altitude, [:math:`m`]
161
+ - ``total_fuel_burn``: total fuel burn, [:math:`kg`]
162
+ """
163
+
164
+ def simulate_fuel_and_performance(
165
+ self,
166
+ *,
167
+ aircraft_type: str,
168
+ altitude_ft: npt.NDArray[np.floating],
169
+ time: npt.NDArray[np.datetime64],
170
+ true_airspeed: npt.NDArray[np.floating],
171
+ air_temperature: npt.NDArray[np.floating],
172
+ aircraft_mass: npt.NDArray[np.floating] | float | None,
173
+ thrust: npt.NDArray[np.floating] | float | None,
174
+ engine_efficiency: npt.NDArray[np.floating] | float | None,
175
+ fuel_flow: npt.NDArray[np.floating] | float | None,
176
+ q_fuel: float,
177
+ n_iter: int,
178
+ amass_oew: float,
179
+ amass_mtow: float,
180
+ amass_mpl: float,
181
+ load_factor: float,
182
+ takeoff_mass: float | None,
183
+ **kwargs: Any,
184
+ ) -> AircraftPerformanceData:
185
+ r"""
186
+ Calculate aircraft mass, fuel mass flow rate, and overall propulsion efficiency.
187
+
188
+ This method performs ``n_iter`` iterations, each of
189
+ which calls :meth:`calculate_aircraft_performance`. Each successive
190
+ iteration generates a better estimate for mass fuel flow rate and aircraft
191
+ mass at each waypoint.
192
+
193
+ Parameters
194
+ ----------
195
+ aircraft_type: str
196
+ Aircraft type designator used to query the underlying model database.
197
+ altitude_ft: npt.NDArray[np.floating]
198
+ Altitude at each waypoint, [:math:`ft`]
199
+ time: npt.NDArray[np.datetime64]
200
+ Waypoint time in ``np.datetime64`` format.
201
+ true_airspeed: npt.NDArray[np.floating]
202
+ True airspeed for each waypoint, [:math:`m s^{-1}`]
203
+ air_temperature : npt.NDArray[np.floating]
204
+ Ambient temperature for each waypoint, [:math:`K`]
205
+ aircraft_mass : npt.NDArray[np.floating] | float | None
206
+ Override the aircraft_mass at each waypoint, [:math:`kg`].
207
+ thrust : npt.NDArray[np.floating] | float | None
208
+ Override the thrust setting at each waypoint, [:math: `N`].
209
+ engine_efficiency : npt.NDArray[np.floating] | float | None
210
+ Override the engine efficiency at each waypoint.
211
+ fuel_flow : npt.NDArray[np.floating] | float | None
212
+ Override the fuel flow at each waypoint, [:math:`kg s^{-1}`].
213
+ q_fuel : float
214
+ Lower calorific value (LCV) of fuel, [:math:`J \ kg_{fuel}^{-1}`].
215
+ amass_oew : float
216
+ Aircraft operating empty weight, [:math:`kg`]. Used to determine
217
+ the initial aircraft mass if ``takeoff_mass`` is not provided.
218
+ This quantity is constant for a given aircraft type.
219
+ amass_mtow : float
220
+ Aircraft maximum take-off weight, [:math:`kg`]. Used to determine
221
+ the initial aircraft mass if ``takeoff_mass`` is not provided.
222
+ This quantity is constant for a given aircraft type.
223
+ amass_mpl : float
224
+ Aircraft maximum payload, [:math:`kg`]. Used to determine
225
+ the initial aircraft mass if ``takeoff_mass`` is not provided.
226
+ This quantity is constant for a given aircraft type.
227
+ load_factor : float
228
+ Aircraft load factor assumption (between 0 and 1). If unknown,
229
+ a value of 0.7 is a reasonable default. Typically, this parameter
230
+ is between 0.6 and 0.8. During the height of the COVID-19 pandemic,
231
+ this parameter was often much lower.
232
+ takeoff_mass : float | None, optional
233
+ If known, the takeoff mass can be provided to skip the calculation
234
+ in :func:`jet.initial_aircraft_mass`. In this case, the parameters
235
+ ``load_factor``, ``amass_oew``, ``amass_mtow``, and ``amass_mpl`` are
236
+ ignored.
237
+ **kwargs : Any
238
+ Additional keyword arguments are passed to :meth:`calculate_aircraft_performance`.
239
+
240
+ Returns
241
+ -------
242
+ AircraftPerformanceData
243
+ Results from the final iteration is returned.
244
+ """
245
+
246
+ # shortcut if aircraft mass is provided
247
+ if aircraft_mass is not None:
248
+ return self._simulate_fuel_and_performance_known_aircraft_mass(
249
+ aircraft_type=aircraft_type,
250
+ altitude_ft=altitude_ft,
251
+ time=time,
252
+ true_airspeed=true_airspeed,
253
+ air_temperature=air_temperature,
254
+ aircraft_mass=aircraft_mass,
255
+ thrust=thrust,
256
+ engine_efficiency=engine_efficiency,
257
+ fuel_flow=fuel_flow,
258
+ q_fuel=q_fuel,
259
+ **kwargs,
260
+ )
261
+
262
+ return self._simulate_fuel_and_performance_unknown_aircraft_mass(
263
+ aircraft_type=aircraft_type,
264
+ altitude_ft=altitude_ft,
265
+ time=time,
266
+ true_airspeed=true_airspeed,
267
+ air_temperature=air_temperature,
268
+ thrust=thrust,
269
+ engine_efficiency=engine_efficiency,
270
+ fuel_flow=fuel_flow,
271
+ q_fuel=q_fuel,
272
+ n_iter=n_iter,
273
+ amass_oew=amass_oew,
274
+ amass_mtow=amass_mtow,
275
+ amass_mpl=amass_mpl,
276
+ load_factor=load_factor,
277
+ takeoff_mass=takeoff_mass,
278
+ **kwargs,
279
+ )
280
+
281
+ def _simulate_fuel_and_performance_known_aircraft_mass(
282
+ self,
283
+ *,
284
+ aircraft_type: str,
285
+ altitude_ft: npt.NDArray[np.floating],
286
+ time: npt.NDArray[np.datetime64],
287
+ true_airspeed: npt.NDArray[np.floating],
288
+ air_temperature: npt.NDArray[np.floating],
289
+ aircraft_mass: npt.NDArray[np.floating] | float,
290
+ thrust: npt.NDArray[np.floating] | float | None,
291
+ engine_efficiency: npt.NDArray[np.floating] | float | None,
292
+ fuel_flow: npt.NDArray[np.floating] | float | None,
293
+ q_fuel: float,
294
+ **kwargs: Any,
295
+ ) -> AircraftPerformanceData:
296
+ # If fuel_flow is None and a non-constant aircraft_mass is provided
297
+ # at each waypoint, then assume that the derivative with respect to
298
+ # time is the fuel flow rate.
299
+ if fuel_flow is None and isinstance(aircraft_mass, np.ndarray):
300
+ d_aircraft_mass = np.diff(aircraft_mass)
301
+
302
+ if np.any(d_aircraft_mass > 0.0):
303
+ warnings.warn(
304
+ "There are increases in aircraft mass between waypoints. This is not expected."
305
+ )
306
+
307
+ # Only proceed if aircraft mass is decreasing somewhere
308
+ # This excludes a constant aircraft mass
309
+ if np.any(d_aircraft_mass < 0.0):
310
+ if not np.all(d_aircraft_mass < 0.0):
311
+ warnings.warn(
312
+ "Aircraft mass is being used to compute fuel flow, but the "
313
+ "aircraft mass is not monotonically decreasing. This may "
314
+ "result in incorrect fuel flow calculations."
315
+ )
316
+ segment_duration = flight.segment_duration(time, dtype=aircraft_mass.dtype)
317
+ fuel_flow = -np.append(d_aircraft_mass, np.float32(np.nan)) / segment_duration
318
+
319
+ return self.calculate_aircraft_performance(
320
+ aircraft_type=aircraft_type,
321
+ altitude_ft=altitude_ft,
322
+ air_temperature=air_temperature,
323
+ time=time,
324
+ true_airspeed=true_airspeed,
325
+ aircraft_mass=aircraft_mass,
326
+ engine_efficiency=engine_efficiency,
327
+ fuel_flow=fuel_flow,
328
+ thrust=thrust,
329
+ q_fuel=q_fuel,
330
+ **kwargs,
331
+ )
332
+
333
+ def _simulate_fuel_and_performance_unknown_aircraft_mass(
334
+ self,
335
+ *,
336
+ aircraft_type: str,
337
+ altitude_ft: npt.NDArray[np.floating],
338
+ time: npt.NDArray[np.datetime64],
339
+ true_airspeed: npt.NDArray[np.floating],
340
+ air_temperature: npt.NDArray[np.floating],
341
+ thrust: npt.NDArray[np.floating] | float | None,
342
+ engine_efficiency: npt.NDArray[np.floating] | float | None,
343
+ fuel_flow: npt.NDArray[np.floating] | float | None,
344
+ q_fuel: float,
345
+ n_iter: int,
346
+ amass_oew: float,
347
+ amass_mtow: float,
348
+ amass_mpl: float,
349
+ load_factor: float,
350
+ takeoff_mass: float | None,
351
+ **kwargs: Any,
352
+ ) -> AircraftPerformanceData:
353
+ # Variable aircraft_mass will change dynamically after each iteration
354
+ # Set the initial aircraft mass depending on a possible load factor
355
+
356
+ aircraft_mass: npt.NDArray[np.floating] | float
357
+ if takeoff_mass is not None:
358
+ aircraft_mass = takeoff_mass
359
+ else:
360
+ # The initial aircraft mass gets updated at each iteration
361
+ # The exact value here is not important
362
+ aircraft_mass = amass_oew + load_factor * (amass_mtow - amass_oew)
363
+
364
+ for _ in range(n_iter):
365
+ aircraft_performance = self.calculate_aircraft_performance(
366
+ aircraft_type=aircraft_type,
367
+ altitude_ft=altitude_ft,
368
+ air_temperature=air_temperature,
369
+ time=time,
370
+ true_airspeed=true_airspeed,
371
+ aircraft_mass=aircraft_mass,
372
+ engine_efficiency=engine_efficiency,
373
+ fuel_flow=fuel_flow,
374
+ thrust=thrust,
375
+ q_fuel=q_fuel,
376
+ **kwargs,
377
+ )
378
+
379
+ # The max value in the BADA tables is 4.6 kg/s per engine.
380
+ # Multiplying this by 4 engines and giving a buffer.
381
+ if np.any(aircraft_performance.fuel_flow > 25.0):
382
+ raise RuntimeError(
383
+ "Model failure: fuel mass flow rate is unrealistic and the "
384
+ "built-in guardrails are not working."
385
+ )
386
+
387
+ tot_reserve_fuel = jet.reserve_fuel_requirements(
388
+ aircraft_performance.rocd,
389
+ altitude_ft,
390
+ aircraft_performance.fuel_flow,
391
+ aircraft_performance.fuel_burn,
392
+ )
393
+
394
+ aircraft_mass = jet.update_aircraft_mass(
395
+ operating_empty_weight=amass_oew,
396
+ max_takeoff_weight=amass_mtow,
397
+ max_payload=amass_mpl,
398
+ fuel_burn=aircraft_performance.fuel_burn,
399
+ total_reserve_fuel=tot_reserve_fuel,
400
+ load_factor=load_factor,
401
+ takeoff_mass=takeoff_mass,
402
+ )
403
+
404
+ # Update aircraft mass to the latest fuel consumption estimate
405
+ # As long as the for-loop is entered, the aircraft mass will be
406
+ # a numpy array.
407
+ aircraft_performance.aircraft_mass = aircraft_mass # type: ignore[assignment]
408
+
409
+ return aircraft_performance
410
+
411
+ @abc.abstractmethod
412
+ def calculate_aircraft_performance(
413
+ self,
414
+ *,
415
+ aircraft_type: str,
416
+ altitude_ft: npt.NDArray[np.floating],
417
+ air_temperature: npt.NDArray[np.floating],
418
+ time: npt.NDArray[np.datetime64] | None,
419
+ true_airspeed: npt.NDArray[np.floating] | float | None,
420
+ aircraft_mass: npt.NDArray[np.floating] | float,
421
+ engine_efficiency: npt.NDArray[np.floating] | float | None,
422
+ fuel_flow: npt.NDArray[np.floating] | float | None,
423
+ thrust: npt.NDArray[np.floating] | float | None,
424
+ q_fuel: float,
425
+ **kwargs: Any,
426
+ ) -> AircraftPerformanceData:
427
+ r"""
428
+ Calculate aircraft performance along a trajectory.
429
+
430
+ When ``time`` is not None, this method should be used for a single flight
431
+ trajectory. Waypoints are coupled via the ``time`` parameter.
432
+
433
+ This method computes the rate of climb and descent (ROCD) to determine
434
+ flight phases: "cruise", "climb", and "descent". Performance metrics
435
+ depend on this phase.
436
+
437
+ When ``time`` is None, this method can be used to simulate flight performance
438
+ over an arbitrary sequence of flight waypoints by assuming nominal flight
439
+ characteristics. In this case, each point is treated independently and
440
+ all points are assumed to be in a "cruise" phase of the flight.
441
+
442
+ Parameters
443
+ ----------
444
+ aircraft_type : str
445
+ Used to query the underlying model database for aircraft engine parameters.
446
+ altitude_ft : npt.NDArray[np.floating]
447
+ Altitude at each waypoint, [:math:`ft`]
448
+ air_temperature : npt.NDArray[np.floating]
449
+ Ambient temperature for each waypoint, [:math:`K`]
450
+ time: npt.NDArray[np.datetime64] | None
451
+ Waypoint time in ``np.datetime64`` format. If None, only drag force
452
+ will is used in thrust calculations (ie, no vertical change and constant
453
+ horizontal change). In addition, aircraft is assumed to be in cruise.
454
+ true_airspeed : npt.NDArray[np.floating] | float | None
455
+ True airspeed for each waypoint, [:math:`m s^{-1}`].
456
+ If None, a nominal value is used.
457
+ aircraft_mass : npt.NDArray[np.floating] | float
458
+ Aircraft mass for each waypoint, [:math:`kg`].
459
+ engine_efficiency : npt.NDArray[np.floating] | float | None
460
+ Override the engine efficiency at each waypoint.
461
+ fuel_flow : npt.NDArray[np.floating] | float | None
462
+ Override the fuel flow at each waypoint, [:math:`kg s^{-1}`].
463
+ thrust : npt.NDArray[np.floating] | float | None
464
+ Override the thrust setting at each waypoint, [:math: `N`].
465
+ q_fuel : float
466
+ Lower calorific value (LCV) of fuel, [:math:`J \ kg_{fuel}^{-1}`].
467
+ **kwargs : Any
468
+ Additional keyword arguments to pass to the model.
469
+
470
+ Returns
471
+ -------
472
+ AircraftPerformanceData
473
+ Derived performance metrics at each waypoint.
474
+ """
475
+
476
+ def ensure_air_temperature_on_source(self) -> None:
477
+ """Add ``air_temperature`` field to :attr:`source` data if not already present.
478
+
479
+ This function operates in-place. If ``air_temperature`` is not already present
480
+ on :attr:`source`, it is calculated by interpolation from met data.
481
+ """
482
+ fill_with_isa = self.params["fill_low_altitude_with_isa_temperature"]
483
+
484
+ if "air_temperature" in self.source:
485
+ if not fill_with_isa:
486
+ return
487
+ _fill_low_altitude_with_isa_temperature(self.source, 0.0)
488
+ return
489
+
490
+ temp_available = self.met is not None and "air_temperature" in self.met
491
+
492
+ if not temp_available:
493
+ if fill_with_isa:
494
+ self.source["air_temperature"] = self.source.T_isa()
495
+ return
496
+ msg = (
497
+ "Cannot compute air temperature without providing met data that includes an "
498
+ "'air_temperature' variable. Either include met data with 'air_temperature' "
499
+ "in the model constructor, define 'air_temperature' data on the flight, or set "
500
+ "'fill_low_altitude_with_isa_temperature' to True."
501
+ )
502
+ raise ValueError(msg)
503
+
504
+ interpolate_met(self.met, self.source, "air_temperature", **self.interp_kwargs)
505
+
506
+ if not fill_with_isa:
507
+ return
508
+
509
+ met_level_0 = self.met.data["level"][-1].item() # type: ignore[union-attr]
510
+ _fill_low_altitude_with_isa_temperature(self.source, met_level_0)
511
+
512
+ def ensure_true_airspeed_on_source(self) -> None:
513
+ """Add ``true_airspeed`` field to :attr:`source` data if not already present.
514
+
515
+ This function operates in-place. If ``true_airspeed`` is not already present
516
+ on :attr:`source`, it is calculated using :meth:`Flight.segment_true_airspeed`.
517
+ """
518
+ tas = self.source.get("true_airspeed")
519
+ fill_with_groundspeed = self.params["fill_low_altitude_with_zero_wind"]
520
+
521
+ if tas is not None:
522
+ if not fill_with_groundspeed:
523
+ return
524
+ cond = np.isnan(tas)
525
+ tas[cond] = self.source.segment_groundspeed()[cond]
526
+ return
527
+
528
+ # Use current cocip convention: eastward_wind on met, u_wind on source
529
+ wind_available = ("u_wind" in self.source and "v_wind" in self.source) or (
530
+ self.met is not None and "eastward_wind" in self.met and "northward_wind" in self.met
531
+ )
532
+
533
+ if not wind_available:
534
+ if fill_with_groundspeed:
535
+ tas = self.source.segment_groundspeed()
536
+ self.source["true_airspeed"] = tas
537
+ return
538
+ msg = (
539
+ "Cannot compute 'true_airspeed' without 'eastward_wind' and 'northward_wind' "
540
+ "met data. Either include met data in the model constructor, define "
541
+ "'true_airspeed' data on the flight, or set "
542
+ "'fill_low_altitude_with_zero_wind' to True."
543
+ )
544
+ raise ValueError(msg)
545
+
546
+ u = interpolate_met(self.met, self.source, "eastward_wind", "u_wind", **self.interp_kwargs)
547
+ v = interpolate_met(self.met, self.source, "northward_wind", "v_wind", **self.interp_kwargs)
548
+
549
+ if fill_with_groundspeed:
550
+ if self.met is None:
551
+ cond = np.isnan(u) & np.isnan(v)
552
+ else:
553
+ met_level_max = self.met.data["level"][-1].item()
554
+ cond = self.source.level > met_level_max
555
+
556
+ # We DON'T overwrite the original u and v arrays already attached to the source
557
+ u = np.where(cond, 0.0, u)
558
+ v = np.where(cond, 0.0, v)
559
+
560
+ out = self.source.segment_true_airspeed(u, v)
561
+ self.source["true_airspeed"] = out
562
+
563
+
564
+ @dataclasses.dataclass
565
+ class AircraftPerformanceData:
566
+ """Store the computed aircraft performance metrics.
567
+
568
+ Parameters
569
+ ----------
570
+ fuel_flow : npt.NDArray[np.floating]
571
+ Fuel mass flow rate for each waypoint, [:math:`kg s^{-1}`]
572
+ aircraft_mass : npt.NDArray[np.floating]
573
+ Aircraft mass for each waypoint, [:math:`kg`]
574
+ true_airspeed : npt.NDArray[np.floating]
575
+ True airspeed at each waypoint, [:math: `m s^{-1}`]
576
+ fuel_burn: npt.NDArray[np.floating]
577
+ Fuel consumption for each waypoint, [:math:`kg`]. Set to an array of
578
+ all nan values if it cannot be computed (ie, working with gridpoints).
579
+ thrust: npt.NDArray[np.floating]
580
+ Thrust force, [:math:`N`]
581
+ engine_efficiency: npt.NDArray[np.floating]
582
+ Overall propulsion efficiency for each waypoint
583
+ rocd : npt.NDArray[np.floating]
584
+ Rate of climb and descent, [:math:`ft min^{-1}`]
585
+ """
586
+
587
+ fuel_flow: npt.NDArray[np.floating]
588
+ aircraft_mass: npt.NDArray[np.floating]
589
+ true_airspeed: npt.NDArray[np.floating]
590
+ fuel_burn: npt.NDArray[np.floating]
591
+ thrust: npt.NDArray[np.floating]
592
+ engine_efficiency: npt.NDArray[np.floating]
593
+ rocd: npt.NDArray[np.floating]
594
+
595
+
596
+ # --------------------------------
597
+ # Grid aircraft performance models
598
+ # --------------------------------
599
+
600
+
601
+ @dataclasses.dataclass
602
+ class AircraftPerformanceGridParams(ModelParams, CommonAircraftPerformanceParams):
603
+ """Parameters for :class:`AircraftPerformanceGrid`."""
604
+
605
+ #: Fuel type
606
+ fuel: fuel.Fuel = dataclasses.field(default_factory=fuel.JetA)
607
+
608
+ #: ICAO code designating simulated aircraft type.
609
+ #: Can be overridden by including ``aircraft_type`` attribute in source data
610
+ aircraft_type: str = "B737"
611
+
612
+ #: Mach number, [:math:`Ma`]
613
+ #: If ``None``, a nominal cruise value is determined by the implementation.
614
+ #: Can be overridden by including a ``mach_number`` key in source data
615
+ mach_number: float | None = None
616
+
617
+ #: Aircraft mass, [:math:`kg`]
618
+ #: If ``None``, a nominal value is determined by the implementation.
619
+ #: Can be overridden by including an ``aircraft_mass`` key in source data
620
+ aircraft_mass: float | None = None
621
+
622
+
623
+ class AircraftPerformanceGrid(Model):
624
+ """
625
+ Support for standardizing aircraft performance methodologies on a grid.
626
+
627
+ Currently just a container until additional models are implemented.
628
+ """
629
+
630
+ @overload
631
+ @abc.abstractmethod
632
+ def eval(self, source: GeoVectorDataset, **params: Any) -> GeoVectorDataset: ...
633
+
634
+ @overload
635
+ @abc.abstractmethod
636
+ def eval(self, source: MetDataset | None = ..., **params: Any) -> MetDataset: ...
637
+
638
+ @abc.abstractmethod
639
+ def eval(
640
+ self, source: GeoVectorDataset | MetDataset | None = None, **params: Any
641
+ ) -> GeoVectorDataset | MetDataset:
642
+ """Evaluate the aircraft performance model."""
643
+
644
+
645
+ @dataclasses.dataclass
646
+ class AircraftPerformanceGridData(Generic[ArrayOrFloat]):
647
+ """Store the computed aircraft performance metrics for nominal cruise conditions."""
648
+
649
+ #: Fuel mass flow rate, [:math:`kg s^{-1}`]
650
+ fuel_flow: ArrayOrFloat
651
+
652
+ #: Engine efficiency, [:math:`0-1`]
653
+ engine_efficiency: ArrayOrFloat
654
+
655
+
656
+ def _fill_low_altitude_with_isa_temperature(vector: GeoVectorDataset, met_level_max: float) -> None:
657
+ """Fill low-altitude NaN values in ``air_temperature`` with ISA values.
658
+
659
+ The ``air_temperature`` param is assumed to have been computed by
660
+ interpolating against a gridded air temperature field that did not
661
+ necessarily extend to the surface. This function fills points below the
662
+ lowest altitude in the gridded data with ISA temperature values.
663
+
664
+ This function operates in-place and modifies the ``air_temperature`` field.
665
+
666
+ Parameters
667
+ ----------
668
+ vector : GeoVectorDataset
669
+ GeoVectorDataset instance associated with the ``air_temperature`` data.
670
+ met_level_max : float
671
+ The maximum level in the met data, [:math:`hPa`].
672
+ """
673
+ air_temperature = vector["air_temperature"]
674
+ is_nan = np.isnan(air_temperature)
675
+ low_alt = vector.level > met_level_max
676
+ cond = is_nan & low_alt
677
+
678
+ t_isa = vector.T_isa()
679
+ air_temperature[cond] = t_isa[cond]