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.
- 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 +2314 -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.cpython-312-darwin.so +0 -0
- pycontrails/core/vector.py +2190 -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 +746 -0
- pycontrails/datalib/ecmwf/__init__.py +73 -0
- pycontrails/datalib/ecmwf/arco_era5.py +340 -0
- pycontrails/datalib/ecmwf/common.py +109 -0
- pycontrails/datalib/ecmwf/era5.py +550 -0
- pycontrails/datalib/ecmwf/era5_model_level.py +487 -0
- pycontrails/datalib/ecmwf/hres.py +782 -0
- pycontrails/datalib/ecmwf/hres_model_level.py +459 -0
- pycontrails/datalib/ecmwf/ifs.py +284 -0
- pycontrails/datalib/ecmwf/model_levels.py +434 -0
- pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
- pycontrails/datalib/ecmwf/variables.py +267 -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 +569 -0
- pycontrails/datalib/sentinel.py +511 -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 +430 -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 +982 -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 +2616 -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 +494 -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.54.0.dist-info/LICENSE +178 -0
- pycontrails-0.54.0.dist-info/METADATA +179 -0
- pycontrails-0.54.0.dist-info/NOTICE +43 -0
- pycontrails-0.54.0.dist-info/RECORD +109 -0
- pycontrails-0.54.0.dist-info/WHEEL +5 -0
- pycontrails-0.54.0.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
"""Support for the Poll-Schumann (PS) theoretical aircraft performance over a grid."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import warnings
|
|
7
|
+
from typing import Any, overload
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import numpy.typing as npt
|
|
11
|
+
import scipy.optimize
|
|
12
|
+
import xarray as xr
|
|
13
|
+
import xarray.core.coordinates as xrcc
|
|
14
|
+
|
|
15
|
+
from pycontrails.core.aircraft_performance import (
|
|
16
|
+
AircraftPerformanceGrid,
|
|
17
|
+
AircraftPerformanceGridData,
|
|
18
|
+
AircraftPerformanceGridParams,
|
|
19
|
+
)
|
|
20
|
+
from pycontrails.core.flight import Flight
|
|
21
|
+
from pycontrails.core.fuel import JetA
|
|
22
|
+
from pycontrails.core.met import MetDataset
|
|
23
|
+
from pycontrails.core.met_var import AirTemperature
|
|
24
|
+
from pycontrails.core.vector import GeoVectorDataset
|
|
25
|
+
from pycontrails.models.ps_model import ps_model, ps_operational_limits
|
|
26
|
+
from pycontrails.models.ps_model.ps_aircraft_params import PSAircraftEngineParams
|
|
27
|
+
from pycontrails.physics import units
|
|
28
|
+
from pycontrails.utils.types import ArrayOrFloat
|
|
29
|
+
|
|
30
|
+
# mypy: disable-error-code = type-var
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclasses.dataclass
|
|
34
|
+
class PSGridParams(AircraftPerformanceGridParams):
|
|
35
|
+
"""Parameters for :class:`PSGrid`."""
|
|
36
|
+
|
|
37
|
+
#: Passed into :func:`ps_nominal_grid`
|
|
38
|
+
maxiter: int = 10
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PSGrid(AircraftPerformanceGrid):
|
|
42
|
+
"""Compute nominal Poll-Schumann aircraft performance over a grid.
|
|
43
|
+
|
|
44
|
+
For a given aircraft type, altitude, aircraft mass, air temperature, and
|
|
45
|
+
mach number, the PS model computes a theoretical engine efficiency and fuel
|
|
46
|
+
flow rate for an aircraft under cruise conditions. Letting the aircraft mass
|
|
47
|
+
vary and fixing the other parameters, the engine efficiency curve attains a
|
|
48
|
+
single maximum at a particular aircraft mass. By solving this implicit
|
|
49
|
+
equation, the PS model can be used to compute the aircraft mass that
|
|
50
|
+
maximizes engine efficiency for a given set of parameters. This is the
|
|
51
|
+
"nominal" aircraft mass computed by this model.
|
|
52
|
+
|
|
53
|
+
This nominal aircraft mass is not always realizable. For example, the maximum
|
|
54
|
+
engine efficiency may be attained at an aircraft mass that is less than the
|
|
55
|
+
operating empty mass of the aircraft. This model determines the minimum and
|
|
56
|
+
maximum possible aircraft mass for a given set of parameters using a simple
|
|
57
|
+
heuristic. The nominal aircraft mass is then clipped to this range.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
name = "PSGrid"
|
|
61
|
+
long_name = "Poll-Schumann Aircraft Performance evaluated at arbitrary points"
|
|
62
|
+
met_variables = (AirTemperature,)
|
|
63
|
+
default_params = PSGridParams
|
|
64
|
+
|
|
65
|
+
met: MetDataset
|
|
66
|
+
source: GeoVectorDataset
|
|
67
|
+
|
|
68
|
+
@overload
|
|
69
|
+
def eval(self, source: GeoVectorDataset, **params: Any) -> GeoVectorDataset: ...
|
|
70
|
+
|
|
71
|
+
@overload
|
|
72
|
+
def eval(self, source: MetDataset | None = ..., **params: Any) -> MetDataset: ...
|
|
73
|
+
|
|
74
|
+
def eval(
|
|
75
|
+
self, source: GeoVectorDataset | MetDataset | None = None, **params: Any
|
|
76
|
+
) -> GeoVectorDataset | MetDataset:
|
|
77
|
+
"""Evaluate the PS model over a :class:`MetDataset` or :class:`GeoVectorDataset`.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
source : GeoVectorDataset | MetDataset | None, optional
|
|
82
|
+
The source data to use for the evaluation. If None, the source is taken
|
|
83
|
+
from the :attr:`met` attribute of the :class:`PSGrid` instance.
|
|
84
|
+
The aircraft type is taken from ``source.attrs["aircraft_type"]``. If this field
|
|
85
|
+
is not present, ``params["aircraft_type"]`` is used instead. See the
|
|
86
|
+
static CSV file :file:`ps-aircraft-params-20240524.csv` for a list of supported
|
|
87
|
+
aircraft types.
|
|
88
|
+
**params : Any
|
|
89
|
+
Override the default parameters of the :class:`PSGrid` instance.
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
GeoVectorDataset | MetDataset
|
|
94
|
+
The source data with the following variables added:
|
|
95
|
+
|
|
96
|
+
- aircraft_mass
|
|
97
|
+
- fuel_flow
|
|
98
|
+
- engine_efficiency
|
|
99
|
+
|
|
100
|
+
Raises
|
|
101
|
+
------
|
|
102
|
+
NotImplementedError
|
|
103
|
+
If "true_airspeed" or "aircraft_mass" fields are included in
|
|
104
|
+
:attr:`source`.
|
|
105
|
+
|
|
106
|
+
See Also
|
|
107
|
+
--------
|
|
108
|
+
:func:`ps_nominal_grid`
|
|
109
|
+
"""
|
|
110
|
+
self.update_params(**params)
|
|
111
|
+
self.set_source(source)
|
|
112
|
+
self.require_source_type((GeoVectorDataset, MetDataset))
|
|
113
|
+
self.set_source_met()
|
|
114
|
+
|
|
115
|
+
# Check some assumptions
|
|
116
|
+
if "true_airspeed" in self.source or "true_airspeed" in self.source.attrs:
|
|
117
|
+
msg = "PSGrid currently only supports setting a 'mach_number' parameter."
|
|
118
|
+
raise NotImplementedError(msg)
|
|
119
|
+
if self.get_source_param("aircraft_mass", set_attr=False) is not None:
|
|
120
|
+
msg = "The 'aircraft_mass' parameter must be None."
|
|
121
|
+
raise NotImplementedError(msg)
|
|
122
|
+
if isinstance(self.source, Flight):
|
|
123
|
+
warnings.warn(
|
|
124
|
+
"The 'PSGrid' model is not intended to support 'Flight' objects as 'source' "
|
|
125
|
+
"data. Instead, use the 'PSFlight' model."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Extract the relevant source data
|
|
129
|
+
try:
|
|
130
|
+
aircraft_type = self.source.attrs["aircraft_type"]
|
|
131
|
+
except KeyError:
|
|
132
|
+
aircraft_type = self.params["aircraft_type"]
|
|
133
|
+
self.source.attrs["aircraft_type"] = aircraft_type
|
|
134
|
+
|
|
135
|
+
fuel = self.source.attrs.get("fuel", self.params["fuel"])
|
|
136
|
+
q_fuel = fuel.q_fuel
|
|
137
|
+
mach_number = self.get_source_param("mach_number", set_attr=False)
|
|
138
|
+
|
|
139
|
+
if isinstance(self.source, MetDataset):
|
|
140
|
+
if "fuel_flow" in self.source:
|
|
141
|
+
msg = "PSGrid doesn't support custom 'fuel_flow' values."
|
|
142
|
+
raise NotImplementedError(msg)
|
|
143
|
+
if "engine_efficiency" in self.source:
|
|
144
|
+
msg = "PSGrid doesn't support custom 'engine_efficiency' values."
|
|
145
|
+
raise NotImplementedError(msg)
|
|
146
|
+
|
|
147
|
+
ds = ps_nominal_grid(
|
|
148
|
+
aircraft_type,
|
|
149
|
+
air_temperature=self.source.data["air_temperature"],
|
|
150
|
+
q_fuel=q_fuel,
|
|
151
|
+
mach_number=mach_number,
|
|
152
|
+
maxiter=self.params["maxiter"],
|
|
153
|
+
)
|
|
154
|
+
return MetDataset(ds)
|
|
155
|
+
|
|
156
|
+
air_temperature = self.source["air_temperature"]
|
|
157
|
+
ds = ps_nominal_grid(
|
|
158
|
+
aircraft_type,
|
|
159
|
+
level=self.source.level,
|
|
160
|
+
air_temperature=air_temperature,
|
|
161
|
+
q_fuel=q_fuel,
|
|
162
|
+
mach_number=mach_number,
|
|
163
|
+
maxiter=self.params["maxiter"],
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Set the source data
|
|
167
|
+
self.source.setdefault("aircraft_mass", ds["aircraft_mass"])
|
|
168
|
+
self.source.setdefault("fuel_flow", ds["fuel_flow"])
|
|
169
|
+
self.source.setdefault("engine_efficiency", ds["engine_efficiency"])
|
|
170
|
+
mach_number = self.source.attrs.setdefault("mach_number", ds.attrs["mach_number"])
|
|
171
|
+
self.source["true_airspeed"] = units.mach_number_to_tas(mach_number, air_temperature)
|
|
172
|
+
self.source.attrs.setdefault("wingspan", ds.attrs["wingspan"])
|
|
173
|
+
self.source.attrs.setdefault("n_engine", ds.attrs["n_engine"])
|
|
174
|
+
|
|
175
|
+
return self.source
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclasses.dataclass
|
|
179
|
+
class _PerfVariables:
|
|
180
|
+
atyp_param: PSAircraftEngineParams
|
|
181
|
+
air_pressure: npt.NDArray[np.float64] | float
|
|
182
|
+
air_temperature: npt.NDArray[np.float64] | float
|
|
183
|
+
mach_number: npt.NDArray[np.float64] | float
|
|
184
|
+
q_fuel: float
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _nominal_perf(aircraft_mass: ArrayOrFloat, perf: _PerfVariables) -> AircraftPerformanceGridData:
|
|
188
|
+
"""Compute nominal Poll-Schumann aircraft performance."""
|
|
189
|
+
|
|
190
|
+
atyp_param = perf.atyp_param
|
|
191
|
+
air_pressure = perf.air_pressure
|
|
192
|
+
air_temperature = perf.air_temperature
|
|
193
|
+
mach_number = perf.mach_number
|
|
194
|
+
q_fuel = perf.q_fuel
|
|
195
|
+
|
|
196
|
+
theta = 0.0
|
|
197
|
+
dv_dt = 0.0
|
|
198
|
+
|
|
199
|
+
rn = ps_model.reynolds_number(
|
|
200
|
+
atyp_param.wing_surface_area, mach_number, air_temperature, air_pressure
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
c_lift = ps_model.lift_coefficient(
|
|
204
|
+
atyp_param.wing_surface_area, aircraft_mass, air_pressure, mach_number, theta
|
|
205
|
+
)
|
|
206
|
+
c_f = ps_model.skin_friction_coefficient(rn)
|
|
207
|
+
c_drag_0 = ps_model.zero_lift_drag_coefficient(c_f, atyp_param.psi_0)
|
|
208
|
+
e_ls = ps_model.oswald_efficiency_factor(c_drag_0, atyp_param)
|
|
209
|
+
c_drag_w = ps_model.wave_drag_coefficient(mach_number, c_lift, atyp_param)
|
|
210
|
+
c_drag = ps_model.airframe_drag_coefficient(
|
|
211
|
+
c_drag_0, c_drag_w, c_lift, e_ls, atyp_param.wing_aspect_ratio
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
thrust = ps_model.thrust_force(aircraft_mass, c_lift, c_drag, dv_dt, theta)
|
|
215
|
+
|
|
216
|
+
c_t = ps_model.engine_thrust_coefficient(
|
|
217
|
+
thrust, mach_number, air_pressure, atyp_param.wing_surface_area
|
|
218
|
+
)
|
|
219
|
+
c_t_eta_b = ps_model.thrust_coefficient_at_max_efficiency(
|
|
220
|
+
mach_number, atyp_param.m_des, atyp_param.c_t_des
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Always correct thrust coefficients in the gridded case
|
|
224
|
+
# (In the flight case, this correction is governed by a model parameter)
|
|
225
|
+
c_t_available = ps_operational_limits.max_available_thrust_coefficient(
|
|
226
|
+
air_temperature, mach_number, c_t_eta_b, atyp_param
|
|
227
|
+
)
|
|
228
|
+
np.clip(c_t, 0.0, c_t_available, out=c_t)
|
|
229
|
+
|
|
230
|
+
engine_efficiency = ps_model.overall_propulsion_efficiency(
|
|
231
|
+
mach_number, c_t, c_t_eta_b, atyp_param
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
fuel_flow = ps_model.fuel_mass_flow_rate(
|
|
235
|
+
air_pressure,
|
|
236
|
+
air_temperature,
|
|
237
|
+
mach_number,
|
|
238
|
+
c_t,
|
|
239
|
+
engine_efficiency,
|
|
240
|
+
atyp_param.wing_surface_area,
|
|
241
|
+
q_fuel,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return AircraftPerformanceGridData(
|
|
245
|
+
fuel_flow=fuel_flow,
|
|
246
|
+
engine_efficiency=engine_efficiency,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _newton_func(aircraft_mass: ArrayOrFloat, perf: _PerfVariables) -> ArrayOrFloat:
|
|
251
|
+
"""Approximate the derivative of the engine efficiency with respect to mass.
|
|
252
|
+
|
|
253
|
+
This is used to find the mass at which the engine efficiency is maximized.
|
|
254
|
+
"""
|
|
255
|
+
eta1 = _nominal_perf(aircraft_mass + 0.5, perf).engine_efficiency
|
|
256
|
+
eta2 = _nominal_perf(aircraft_mass - 0.5, perf).engine_efficiency
|
|
257
|
+
return eta1 - eta2
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _min_mass(
|
|
261
|
+
oem: float,
|
|
262
|
+
lf: float,
|
|
263
|
+
mpm: float,
|
|
264
|
+
reserve_fuel: float,
|
|
265
|
+
) -> float:
|
|
266
|
+
"""Calculate the minimum mass given OEM, LF, MPM, and reserve fuel."""
|
|
267
|
+
return oem + lf * mpm + reserve_fuel
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _estimate_mass_extremes(
|
|
271
|
+
atyp_param: PSAircraftEngineParams,
|
|
272
|
+
perf: _PerfVariables,
|
|
273
|
+
n_iter: int = 3,
|
|
274
|
+
) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
|
|
275
|
+
"""Calculate the minimum and maximum mass for a given aircraft type."""
|
|
276
|
+
|
|
277
|
+
oem = atyp_param.amass_oew # operating empty mass
|
|
278
|
+
lf = 0.7 # load factor
|
|
279
|
+
mpm = atyp_param.amass_mpl # max payload mass
|
|
280
|
+
mtow = atyp_param.amass_mtow # max takeoff mass
|
|
281
|
+
|
|
282
|
+
min_mass = _min_mass(oem, lf, mpm, 0.0) # no reserve fuel
|
|
283
|
+
for _ in range(n_iter):
|
|
284
|
+
# Estimate the fuel required to cruise at 35,000 ft for 90 minutes.
|
|
285
|
+
# This is used to compute the reserve fuel.
|
|
286
|
+
ff = _nominal_perf(min_mass, perf).fuel_flow
|
|
287
|
+
reserve_fuel = ff * 60.0 * 90.0 # 90 minutes
|
|
288
|
+
min_mass = _min_mass(oem, lf, mpm, reserve_fuel)
|
|
289
|
+
|
|
290
|
+
# Crude: Assume 2x the fuel flow of cruise for climb
|
|
291
|
+
# Compute the maximum weight at cruise by assuming a 20 minute climb
|
|
292
|
+
ff = _nominal_perf(mtow, perf).fuel_flow
|
|
293
|
+
max_mass = mtow - 2.0 * ff * 60.0 * 20.0
|
|
294
|
+
|
|
295
|
+
return min_mass, max_mass # type: ignore[return-value]
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _parse_variables(
|
|
299
|
+
level: npt.NDArray[np.float64] | None,
|
|
300
|
+
air_temperature: xr.DataArray | npt.NDArray[np.float64] | None,
|
|
301
|
+
) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
|
|
302
|
+
"""Parse the level and air temperature arguments."""
|
|
303
|
+
|
|
304
|
+
if isinstance(air_temperature, xr.DataArray):
|
|
305
|
+
if level is not None:
|
|
306
|
+
msg = "If 'air_temperature' is a DataArray, 'level' must be None"
|
|
307
|
+
raise ValueError(msg)
|
|
308
|
+
|
|
309
|
+
level_da = air_temperature["level"]
|
|
310
|
+
air_temperature, level_da = xr.broadcast(air_temperature, level_da)
|
|
311
|
+
return np.asarray(level_da), np.asarray(air_temperature)
|
|
312
|
+
|
|
313
|
+
if air_temperature is None:
|
|
314
|
+
if level is None:
|
|
315
|
+
msg = "The 'level' argument must be specified"
|
|
316
|
+
raise ValueError(msg)
|
|
317
|
+
altitude_m = units.pl_to_m(level)
|
|
318
|
+
air_temperature = units.m_to_T_isa(altitude_m)
|
|
319
|
+
return level, air_temperature
|
|
320
|
+
|
|
321
|
+
if level is None:
|
|
322
|
+
msg = "The 'level' argument must be specified"
|
|
323
|
+
raise ValueError(msg)
|
|
324
|
+
|
|
325
|
+
return level, air_temperature
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def ps_nominal_grid(
|
|
329
|
+
aircraft_type: str,
|
|
330
|
+
*,
|
|
331
|
+
level: npt.NDArray[np.float64] | None = None,
|
|
332
|
+
air_temperature: xr.DataArray | npt.NDArray[np.float64] | None = None,
|
|
333
|
+
q_fuel: float = JetA.q_fuel,
|
|
334
|
+
mach_number: float | None = None,
|
|
335
|
+
maxiter: int = PSGridParams.maxiter,
|
|
336
|
+
) -> xr.Dataset:
|
|
337
|
+
"""Calculate the nominal performance grid for a given aircraft type.
|
|
338
|
+
|
|
339
|
+
This function is similar to the :class:`PSGrid` model, but it doesn't require
|
|
340
|
+
meteorological data. Instead, the ambient air temperature can be computed from
|
|
341
|
+
the ISA model or passed as an argument.
|
|
342
|
+
|
|
343
|
+
Parameters
|
|
344
|
+
----------
|
|
345
|
+
aircraft_type : str
|
|
346
|
+
The aircraft type.
|
|
347
|
+
level : npt.NDArray[np.float64] | None, optional
|
|
348
|
+
The pressure level, [:math:`hPa`]. If None, the ``air_temperature``
|
|
349
|
+
argument must be a :class:`xarray.DataArray` with a ``level`` coordinate.
|
|
350
|
+
air_temperature : xr.DataArray | npt.NDArray[np.float64] | None, optional
|
|
351
|
+
The ambient air temperature, [:math:`K`]. If None (default), the ISA
|
|
352
|
+
temperature is computed from the ``level`` argument. If a :class:`xarray.DataArray`,
|
|
353
|
+
the ``level`` coordinate must be present and the ``level`` argument must be None
|
|
354
|
+
to avoid ambiguity. If a :class:`numpy.ndarray` is passed, it is assumed to be 1
|
|
355
|
+
dimensional with the same shape as the ``level`` argument.
|
|
356
|
+
q_fuel : float, optional
|
|
357
|
+
The fuel heating value, by default :attr:`JetA.q_fuel`
|
|
358
|
+
mach_number : float | None, optional
|
|
359
|
+
The Mach number. If None (default), the PS design Mach number is used.
|
|
360
|
+
maxiter : int, optional
|
|
361
|
+
Passed into :func:`scipy.optimize.newton`.
|
|
362
|
+
|
|
363
|
+
Returns
|
|
364
|
+
-------
|
|
365
|
+
xr.Dataset
|
|
366
|
+
The nominal performance grid. The grid is indexed by altitude and Mach number.
|
|
367
|
+
Contains the following variables:
|
|
368
|
+
|
|
369
|
+
- ``"fuel_flow"`` : Fuel flow rate, [:math:`kg/s`]
|
|
370
|
+
- ``"engine_efficiency"`` : Engine efficiency
|
|
371
|
+
- ``"aircraft_mass"`` : Aircraft mass at which the engine efficiency is maximized,
|
|
372
|
+
[:math:`kg`]
|
|
373
|
+
|
|
374
|
+
Raises
|
|
375
|
+
------
|
|
376
|
+
KeyError
|
|
377
|
+
If "aircraft_type" is not supported by the PS model.
|
|
378
|
+
|
|
379
|
+
Examples
|
|
380
|
+
--------
|
|
381
|
+
>>> level = np.arange(200, 300, 10, dtype=float)
|
|
382
|
+
|
|
383
|
+
>>> # Compute nominal aircraft performance assuming ISA conditions
|
|
384
|
+
>>> # and the design Mach number
|
|
385
|
+
>>> perf = ps_nominal_grid("A320", level=level)
|
|
386
|
+
>>> perf.attrs["mach_number"]
|
|
387
|
+
0.753
|
|
388
|
+
|
|
389
|
+
>>> perf.to_dataframe()
|
|
390
|
+
aircraft_mass engine_efficiency fuel_flow
|
|
391
|
+
level
|
|
392
|
+
200.0 58416.230843 0.300958 0.575635
|
|
393
|
+
210.0 61617.676624 0.300958 0.604417
|
|
394
|
+
220.0 64829.702583 0.300958 0.633199
|
|
395
|
+
230.0 68026.415695 0.300958 0.662998
|
|
396
|
+
240.0 71187.897060 0.300958 0.694631
|
|
397
|
+
250.0 71775.399825 0.300824 0.703349
|
|
398
|
+
260.0 71765.716737 0.300363 0.708259
|
|
399
|
+
270.0 71752.405400 0.299671 0.714514
|
|
400
|
+
280.0 71736.129079 0.298823 0.721878
|
|
401
|
+
290.0 71717.392170 0.297875 0.730169
|
|
402
|
+
|
|
403
|
+
>>> # Now compute it for a higher Mach number
|
|
404
|
+
>>> perf = ps_nominal_grid("A320", level=level, mach_number=0.78)
|
|
405
|
+
>>> perf.to_dataframe()
|
|
406
|
+
aircraft_mass engine_efficiency fuel_flow
|
|
407
|
+
level
|
|
408
|
+
200.0 57941.825236 0.306598 0.596100
|
|
409
|
+
210.0 60626.062062 0.306605 0.621331
|
|
410
|
+
220.0 63818.498306 0.306605 0.650918
|
|
411
|
+
230.0 66993.691517 0.306605 0.681551
|
|
412
|
+
240.0 70129.930503 0.306605 0.714069
|
|
413
|
+
250.0 71703.009059 0.306560 0.732944
|
|
414
|
+
260.0 71690.188652 0.306239 0.739276
|
|
415
|
+
270.0 71673.392089 0.305694 0.747052
|
|
416
|
+
280.0 71653.431321 0.304997 0.755990
|
|
417
|
+
290.0 71630.901315 0.304201 0.765883
|
|
418
|
+
"""
|
|
419
|
+
coords: dict[str, Any] | xrcc.DataArrayCoordinates
|
|
420
|
+
if isinstance(air_temperature, xr.DataArray):
|
|
421
|
+
dims = air_temperature.dims
|
|
422
|
+
coords = air_temperature.coords
|
|
423
|
+
else:
|
|
424
|
+
dims = ("level",)
|
|
425
|
+
coords = {"level": level}
|
|
426
|
+
|
|
427
|
+
level, air_temperature = _parse_variables(level, air_temperature)
|
|
428
|
+
|
|
429
|
+
air_pressure = level * 100.0
|
|
430
|
+
|
|
431
|
+
aircraft_engine_params = ps_model.load_aircraft_engine_params()
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
atyp_param = aircraft_engine_params[aircraft_type]
|
|
435
|
+
except KeyError as exc:
|
|
436
|
+
msg = (
|
|
437
|
+
f"The aircraft type {aircraft_type} is not currently supported by the PS model. "
|
|
438
|
+
f"Available aircraft types are: {list(aircraft_engine_params)}"
|
|
439
|
+
)
|
|
440
|
+
raise KeyError(msg) from exc
|
|
441
|
+
|
|
442
|
+
mach_number = mach_number or atyp_param.m_des
|
|
443
|
+
|
|
444
|
+
perf = _PerfVariables(
|
|
445
|
+
atyp_param=atyp_param,
|
|
446
|
+
air_pressure=air_pressure,
|
|
447
|
+
air_temperature=air_temperature,
|
|
448
|
+
mach_number=mach_number,
|
|
449
|
+
q_fuel=q_fuel,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
min_mass, max_mass = _estimate_mass_extremes(atyp_param, perf)
|
|
453
|
+
|
|
454
|
+
mass_allowed = ps_operational_limits.max_allowable_aircraft_mass(
|
|
455
|
+
air_pressure,
|
|
456
|
+
mach_number=mach_number,
|
|
457
|
+
mach_num_des=atyp_param.m_des,
|
|
458
|
+
c_l_do=atyp_param.c_l_do,
|
|
459
|
+
wing_surface_area=atyp_param.wing_surface_area,
|
|
460
|
+
amass_mtow=atyp_param.amass_mtow,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
min_mass.clip(max=mass_allowed, out=min_mass) # type: ignore[call-overload]
|
|
464
|
+
max_mass.clip(max=mass_allowed, out=max_mass) # type: ignore[call-overload]
|
|
465
|
+
|
|
466
|
+
x0 = np.full_like(air_temperature, (max_mass + min_mass) / 2.0)
|
|
467
|
+
|
|
468
|
+
# Choose aircraft mass to maximize engine efficiency
|
|
469
|
+
# This is the critical step of the calculation
|
|
470
|
+
aircraft_mass = scipy.optimize.newton(
|
|
471
|
+
func=_newton_func,
|
|
472
|
+
args=(perf,),
|
|
473
|
+
x0=x0,
|
|
474
|
+
tol=1.0,
|
|
475
|
+
disp=False,
|
|
476
|
+
maxiter=maxiter,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
# scipy.optimize.newton promotes float32 to float64
|
|
480
|
+
aircraft_mass = aircraft_mass.astype(x0.dtype, copy=False)
|
|
481
|
+
|
|
482
|
+
aircraft_mass.clip(min=min_mass, max=max_mass, out=aircraft_mass)
|
|
483
|
+
|
|
484
|
+
output = _nominal_perf(aircraft_mass, perf)
|
|
485
|
+
|
|
486
|
+
engine_efficiency = output.engine_efficiency
|
|
487
|
+
fuel_flow = output.fuel_flow
|
|
488
|
+
|
|
489
|
+
attrs = {
|
|
490
|
+
"aircraft_type": aircraft_type,
|
|
491
|
+
"mach_number": mach_number,
|
|
492
|
+
"q_fuel": q_fuel,
|
|
493
|
+
"wingspan": atyp_param.wing_span,
|
|
494
|
+
"n_engine": atyp_param.n_engine,
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return xr.Dataset(
|
|
498
|
+
{
|
|
499
|
+
"aircraft_mass": (dims, aircraft_mass),
|
|
500
|
+
"engine_efficiency": (dims, engine_efficiency),
|
|
501
|
+
"fuel_flow": (dims, fuel_flow),
|
|
502
|
+
},
|
|
503
|
+
coords=coords,
|
|
504
|
+
attrs=attrs,
|
|
505
|
+
)
|