pycontrails 0.54.0__cp312-cp312-macosx_10_13_x86_64.whl

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

Potentially problematic release.


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

Files changed (109) hide show
  1. pycontrails/__init__.py +70 -0
  2. pycontrails/_version.py +16 -0
  3. pycontrails/core/__init__.py +30 -0
  4. pycontrails/core/aircraft_performance.py +641 -0
  5. pycontrails/core/airports.py +226 -0
  6. pycontrails/core/cache.py +881 -0
  7. pycontrails/core/coordinates.py +174 -0
  8. pycontrails/core/fleet.py +470 -0
  9. pycontrails/core/flight.py +2314 -0
  10. pycontrails/core/flightplan.py +220 -0
  11. pycontrails/core/fuel.py +140 -0
  12. pycontrails/core/interpolation.py +721 -0
  13. pycontrails/core/met.py +2833 -0
  14. pycontrails/core/met_var.py +307 -0
  15. pycontrails/core/models.py +1181 -0
  16. pycontrails/core/polygon.py +549 -0
  17. pycontrails/core/rgi_cython.cpython-312-darwin.so +0 -0
  18. pycontrails/core/vector.py +2190 -0
  19. pycontrails/datalib/__init__.py +12 -0
  20. pycontrails/datalib/_leo_utils/search.py +250 -0
  21. pycontrails/datalib/_leo_utils/static/bq_roi_query.sql +6 -0
  22. pycontrails/datalib/_leo_utils/vis.py +59 -0
  23. pycontrails/datalib/_met_utils/metsource.py +746 -0
  24. pycontrails/datalib/ecmwf/__init__.py +73 -0
  25. pycontrails/datalib/ecmwf/arco_era5.py +340 -0
  26. pycontrails/datalib/ecmwf/common.py +109 -0
  27. pycontrails/datalib/ecmwf/era5.py +550 -0
  28. pycontrails/datalib/ecmwf/era5_model_level.py +487 -0
  29. pycontrails/datalib/ecmwf/hres.py +782 -0
  30. pycontrails/datalib/ecmwf/hres_model_level.py +459 -0
  31. pycontrails/datalib/ecmwf/ifs.py +284 -0
  32. pycontrails/datalib/ecmwf/model_levels.py +434 -0
  33. pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
  34. pycontrails/datalib/ecmwf/variables.py +267 -0
  35. pycontrails/datalib/gfs/__init__.py +28 -0
  36. pycontrails/datalib/gfs/gfs.py +646 -0
  37. pycontrails/datalib/gfs/variables.py +100 -0
  38. pycontrails/datalib/goes.py +772 -0
  39. pycontrails/datalib/landsat.py +569 -0
  40. pycontrails/datalib/sentinel.py +511 -0
  41. pycontrails/datalib/spire.py +739 -0
  42. pycontrails/ext/bada.py +41 -0
  43. pycontrails/ext/cirium.py +14 -0
  44. pycontrails/ext/empirical_grid.py +140 -0
  45. pycontrails/ext/synthetic_flight.py +430 -0
  46. pycontrails/models/__init__.py +1 -0
  47. pycontrails/models/accf.py +406 -0
  48. pycontrails/models/apcemm/__init__.py +8 -0
  49. pycontrails/models/apcemm/apcemm.py +982 -0
  50. pycontrails/models/apcemm/inputs.py +226 -0
  51. pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
  52. pycontrails/models/apcemm/utils.py +437 -0
  53. pycontrails/models/cocip/__init__.py +29 -0
  54. pycontrails/models/cocip/cocip.py +2616 -0
  55. pycontrails/models/cocip/cocip_params.py +299 -0
  56. pycontrails/models/cocip/cocip_uncertainty.py +285 -0
  57. pycontrails/models/cocip/contrail_properties.py +1517 -0
  58. pycontrails/models/cocip/output_formats.py +2261 -0
  59. pycontrails/models/cocip/radiative_forcing.py +1262 -0
  60. pycontrails/models/cocip/radiative_heating.py +520 -0
  61. pycontrails/models/cocip/unterstrasser_wake_vortex.py +403 -0
  62. pycontrails/models/cocip/wake_vortex.py +396 -0
  63. pycontrails/models/cocip/wind_shear.py +120 -0
  64. pycontrails/models/cocipgrid/__init__.py +9 -0
  65. pycontrails/models/cocipgrid/cocip_grid.py +2573 -0
  66. pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
  67. pycontrails/models/dry_advection.py +494 -0
  68. pycontrails/models/emissions/__init__.py +21 -0
  69. pycontrails/models/emissions/black_carbon.py +594 -0
  70. pycontrails/models/emissions/emissions.py +1353 -0
  71. pycontrails/models/emissions/ffm2.py +336 -0
  72. pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
  73. pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
  74. pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
  75. pycontrails/models/humidity_scaling/__init__.py +37 -0
  76. pycontrails/models/humidity_scaling/humidity_scaling.py +1025 -0
  77. pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
  78. pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
  79. pycontrails/models/issr.py +210 -0
  80. pycontrails/models/pcc.py +327 -0
  81. pycontrails/models/pcr.py +154 -0
  82. pycontrails/models/ps_model/__init__.py +17 -0
  83. pycontrails/models/ps_model/ps_aircraft_params.py +376 -0
  84. pycontrails/models/ps_model/ps_grid.py +505 -0
  85. pycontrails/models/ps_model/ps_model.py +1017 -0
  86. pycontrails/models/ps_model/ps_operational_limits.py +540 -0
  87. pycontrails/models/ps_model/static/ps-aircraft-params-20240524.csv +68 -0
  88. pycontrails/models/ps_model/static/ps-synonym-list-20240524.csv +103 -0
  89. pycontrails/models/sac.py +459 -0
  90. pycontrails/models/tau_cirrus.py +168 -0
  91. pycontrails/physics/__init__.py +1 -0
  92. pycontrails/physics/constants.py +116 -0
  93. pycontrails/physics/geo.py +989 -0
  94. pycontrails/physics/jet.py +837 -0
  95. pycontrails/physics/thermo.py +451 -0
  96. pycontrails/physics/units.py +472 -0
  97. pycontrails/py.typed +0 -0
  98. pycontrails/utils/__init__.py +1 -0
  99. pycontrails/utils/dependencies.py +66 -0
  100. pycontrails/utils/iteration.py +13 -0
  101. pycontrails/utils/json.py +188 -0
  102. pycontrails/utils/temp.py +50 -0
  103. pycontrails/utils/types.py +165 -0
  104. pycontrails-0.54.0.dist-info/LICENSE +178 -0
  105. pycontrails-0.54.0.dist-info/METADATA +179 -0
  106. pycontrails-0.54.0.dist-info/NOTICE +43 -0
  107. pycontrails-0.54.0.dist-info/RECORD +109 -0
  108. pycontrails-0.54.0.dist-info/WHEEL +5 -0
  109. pycontrails-0.54.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,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
+ )