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