pycontrails 0.59.0__cp314-cp314-macosx_10_15_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pycontrails might be problematic. Click here for more details.
- pycontrails/__init__.py +70 -0
- pycontrails/_version.py +34 -0
- pycontrails/core/__init__.py +30 -0
- pycontrails/core/aircraft_performance.py +679 -0
- pycontrails/core/airports.py +228 -0
- pycontrails/core/cache.py +889 -0
- pycontrails/core/coordinates.py +174 -0
- pycontrails/core/fleet.py +483 -0
- pycontrails/core/flight.py +2185 -0
- pycontrails/core/flightplan.py +228 -0
- pycontrails/core/fuel.py +140 -0
- pycontrails/core/interpolation.py +702 -0
- pycontrails/core/met.py +2936 -0
- pycontrails/core/met_var.py +387 -0
- pycontrails/core/models.py +1321 -0
- pycontrails/core/polygon.py +549 -0
- pycontrails/core/rgi_cython.cpython-314-darwin.so +0 -0
- pycontrails/core/vector.py +2249 -0
- pycontrails/datalib/__init__.py +12 -0
- pycontrails/datalib/_met_utils/metsource.py +746 -0
- pycontrails/datalib/ecmwf/__init__.py +73 -0
- pycontrails/datalib/ecmwf/arco_era5.py +345 -0
- pycontrails/datalib/ecmwf/common.py +114 -0
- pycontrails/datalib/ecmwf/era5.py +554 -0
- pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
- pycontrails/datalib/ecmwf/hres.py +804 -0
- pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
- pycontrails/datalib/ecmwf/ifs.py +287 -0
- pycontrails/datalib/ecmwf/model_levels.py +435 -0
- pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
- pycontrails/datalib/ecmwf/variables.py +268 -0
- pycontrails/datalib/geo_utils.py +261 -0
- pycontrails/datalib/gfs/__init__.py +28 -0
- pycontrails/datalib/gfs/gfs.py +656 -0
- pycontrails/datalib/gfs/variables.py +104 -0
- pycontrails/datalib/goes.py +764 -0
- pycontrails/datalib/gruan.py +343 -0
- pycontrails/datalib/himawari/__init__.py +27 -0
- pycontrails/datalib/himawari/header_struct.py +266 -0
- pycontrails/datalib/himawari/himawari.py +671 -0
- pycontrails/datalib/landsat.py +589 -0
- pycontrails/datalib/leo_utils/__init__.py +5 -0
- pycontrails/datalib/leo_utils/correction.py +266 -0
- pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
- pycontrails/datalib/leo_utils/search.py +250 -0
- pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
- pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
- pycontrails/datalib/leo_utils/vis.py +59 -0
- pycontrails/datalib/sentinel.py +650 -0
- pycontrails/datalib/spire/__init__.py +5 -0
- pycontrails/datalib/spire/exceptions.py +62 -0
- pycontrails/datalib/spire/spire.py +604 -0
- pycontrails/ext/bada.py +42 -0
- pycontrails/ext/cirium.py +14 -0
- pycontrails/ext/empirical_grid.py +140 -0
- pycontrails/ext/synthetic_flight.py +431 -0
- pycontrails/models/__init__.py +1 -0
- pycontrails/models/accf.py +425 -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 +2742 -0
- pycontrails/models/cocip/cocip_params.py +305 -0
- pycontrails/models/cocip/cocip_uncertainty.py +291 -0
- pycontrails/models/cocip/contrail_properties.py +1530 -0
- pycontrails/models/cocip/output_formats.py +2270 -0
- pycontrails/models/cocip/radiative_forcing.py +1260 -0
- pycontrails/models/cocip/radiative_heating.py +520 -0
- pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -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 +2552 -0
- pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
- pycontrails/models/dry_advection.py +602 -0
- pycontrails/models/emissions/__init__.py +21 -0
- pycontrails/models/emissions/black_carbon.py +599 -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/extended_k15.py +1327 -0
- pycontrails/models/humidity_scaling/__init__.py +37 -0
- pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -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 +326 -0
- pycontrails/models/pcr.py +154 -0
- pycontrails/models/ps_model/__init__.py +18 -0
- pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
- pycontrails/models/ps_model/ps_grid.py +701 -0
- pycontrails/models/ps_model/ps_model.py +1000 -0
- pycontrails/models/ps_model/ps_operational_limits.py +525 -0
- pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
- pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
- pycontrails/models/sac.py +442 -0
- pycontrails/models/tau_cirrus.py +183 -0
- pycontrails/physics/__init__.py +1 -0
- pycontrails/physics/constants.py +117 -0
- pycontrails/physics/geo.py +1138 -0
- pycontrails/physics/jet.py +968 -0
- pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
- pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
- pycontrails/physics/thermo.py +551 -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 +187 -0
- pycontrails/utils/temp.py +50 -0
- pycontrails/utils/types.py +163 -0
- pycontrails-0.59.0.dist-info/METADATA +179 -0
- pycontrails-0.59.0.dist-info/RECORD +123 -0
- pycontrails-0.59.0.dist-info/WHEEL +6 -0
- pycontrails-0.59.0.dist-info/licenses/LICENSE +178 -0
- pycontrails-0.59.0.dist-info/licenses/NOTICE +43 -0
- pycontrails-0.59.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]
|