pycontrails 0.53.0__cp313-cp313-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pycontrails might be problematic. Click here for more details.
- pycontrails/__init__.py +70 -0
- pycontrails/_version.py +16 -0
- pycontrails/core/__init__.py +30 -0
- pycontrails/core/aircraft_performance.py +641 -0
- pycontrails/core/airports.py +226 -0
- pycontrails/core/cache.py +881 -0
- pycontrails/core/coordinates.py +174 -0
- pycontrails/core/fleet.py +470 -0
- pycontrails/core/flight.py +2312 -0
- pycontrails/core/flightplan.py +220 -0
- pycontrails/core/fuel.py +140 -0
- pycontrails/core/interpolation.py +721 -0
- pycontrails/core/met.py +2833 -0
- pycontrails/core/met_var.py +307 -0
- pycontrails/core/models.py +1181 -0
- pycontrails/core/polygon.py +549 -0
- pycontrails/core/rgi_cython.cp313-win_amd64.pyd +0 -0
- pycontrails/core/vector.py +2191 -0
- pycontrails/datalib/__init__.py +12 -0
- pycontrails/datalib/_leo_utils/search.py +250 -0
- pycontrails/datalib/_leo_utils/static/bq_roi_query.sql +6 -0
- pycontrails/datalib/_leo_utils/vis.py +59 -0
- pycontrails/datalib/_met_utils/metsource.py +743 -0
- pycontrails/datalib/ecmwf/__init__.py +53 -0
- pycontrails/datalib/ecmwf/arco_era5.py +527 -0
- pycontrails/datalib/ecmwf/common.py +109 -0
- pycontrails/datalib/ecmwf/era5.py +538 -0
- pycontrails/datalib/ecmwf/era5_model_level.py +482 -0
- pycontrails/datalib/ecmwf/hres.py +782 -0
- pycontrails/datalib/ecmwf/hres_model_level.py +495 -0
- pycontrails/datalib/ecmwf/ifs.py +284 -0
- pycontrails/datalib/ecmwf/model_levels.py +79 -0
- pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
- pycontrails/datalib/ecmwf/variables.py +256 -0
- pycontrails/datalib/gfs/__init__.py +28 -0
- pycontrails/datalib/gfs/gfs.py +646 -0
- pycontrails/datalib/gfs/variables.py +100 -0
- pycontrails/datalib/goes.py +772 -0
- pycontrails/datalib/landsat.py +568 -0
- pycontrails/datalib/sentinel.py +512 -0
- pycontrails/datalib/spire.py +739 -0
- pycontrails/ext/bada.py +41 -0
- pycontrails/ext/cirium.py +14 -0
- pycontrails/ext/empirical_grid.py +140 -0
- pycontrails/ext/synthetic_flight.py +426 -0
- pycontrails/models/__init__.py +1 -0
- pycontrails/models/accf.py +406 -0
- pycontrails/models/apcemm/__init__.py +8 -0
- pycontrails/models/apcemm/apcemm.py +983 -0
- pycontrails/models/apcemm/inputs.py +226 -0
- pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
- pycontrails/models/apcemm/utils.py +437 -0
- pycontrails/models/cocip/__init__.py +29 -0
- pycontrails/models/cocip/cocip.py +2617 -0
- pycontrails/models/cocip/cocip_params.py +299 -0
- pycontrails/models/cocip/cocip_uncertainty.py +285 -0
- pycontrails/models/cocip/contrail_properties.py +1517 -0
- pycontrails/models/cocip/output_formats.py +2261 -0
- pycontrails/models/cocip/radiative_forcing.py +1262 -0
- pycontrails/models/cocip/radiative_heating.py +520 -0
- pycontrails/models/cocip/unterstrasser_wake_vortex.py +403 -0
- pycontrails/models/cocip/wake_vortex.py +396 -0
- pycontrails/models/cocip/wind_shear.py +120 -0
- pycontrails/models/cocipgrid/__init__.py +9 -0
- pycontrails/models/cocipgrid/cocip_grid.py +2573 -0
- pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
- pycontrails/models/dry_advection.py +486 -0
- pycontrails/models/emissions/__init__.py +21 -0
- pycontrails/models/emissions/black_carbon.py +594 -0
- pycontrails/models/emissions/emissions.py +1353 -0
- pycontrails/models/emissions/ffm2.py +336 -0
- pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
- pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
- pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
- pycontrails/models/humidity_scaling/__init__.py +37 -0
- pycontrails/models/humidity_scaling/humidity_scaling.py +1025 -0
- pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
- pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
- pycontrails/models/issr.py +210 -0
- pycontrails/models/pcc.py +327 -0
- pycontrails/models/pcr.py +154 -0
- pycontrails/models/ps_model/__init__.py +17 -0
- pycontrails/models/ps_model/ps_aircraft_params.py +376 -0
- pycontrails/models/ps_model/ps_grid.py +505 -0
- pycontrails/models/ps_model/ps_model.py +1017 -0
- pycontrails/models/ps_model/ps_operational_limits.py +540 -0
- pycontrails/models/ps_model/static/ps-aircraft-params-20240524.csv +68 -0
- pycontrails/models/ps_model/static/ps-synonym-list-20240524.csv +103 -0
- pycontrails/models/sac.py +459 -0
- pycontrails/models/tau_cirrus.py +168 -0
- pycontrails/physics/__init__.py +1 -0
- pycontrails/physics/constants.py +116 -0
- pycontrails/physics/geo.py +989 -0
- pycontrails/physics/jet.py +837 -0
- pycontrails/physics/thermo.py +451 -0
- pycontrails/physics/units.py +472 -0
- pycontrails/py.typed +0 -0
- pycontrails/utils/__init__.py +1 -0
- pycontrails/utils/dependencies.py +66 -0
- pycontrails/utils/iteration.py +13 -0
- pycontrails/utils/json.py +188 -0
- pycontrails/utils/temp.py +50 -0
- pycontrails/utils/types.py +165 -0
- pycontrails-0.53.0.dist-info/LICENSE +178 -0
- pycontrails-0.53.0.dist-info/METADATA +181 -0
- pycontrails-0.53.0.dist-info/NOTICE +43 -0
- pycontrails-0.53.0.dist-info/RECORD +109 -0
- pycontrails-0.53.0.dist-info/WHEEL +5 -0
- pycontrails-0.53.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]
|