pycontrails 0.58.0__cp314-cp314-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 (122) hide show
  1. pycontrails/__init__.py +70 -0
  2. pycontrails/_version.py +34 -0
  3. pycontrails/core/__init__.py +30 -0
  4. pycontrails/core/aircraft_performance.py +679 -0
  5. pycontrails/core/airports.py +228 -0
  6. pycontrails/core/cache.py +889 -0
  7. pycontrails/core/coordinates.py +174 -0
  8. pycontrails/core/fleet.py +483 -0
  9. pycontrails/core/flight.py +2185 -0
  10. pycontrails/core/flightplan.py +228 -0
  11. pycontrails/core/fuel.py +140 -0
  12. pycontrails/core/interpolation.py +702 -0
  13. pycontrails/core/met.py +2931 -0
  14. pycontrails/core/met_var.py +387 -0
  15. pycontrails/core/models.py +1321 -0
  16. pycontrails/core/polygon.py +549 -0
  17. pycontrails/core/rgi_cython.cpython-314-darwin.so +0 -0
  18. pycontrails/core/vector.py +2249 -0
  19. pycontrails/datalib/__init__.py +12 -0
  20. pycontrails/datalib/_met_utils/metsource.py +746 -0
  21. pycontrails/datalib/ecmwf/__init__.py +73 -0
  22. pycontrails/datalib/ecmwf/arco_era5.py +345 -0
  23. pycontrails/datalib/ecmwf/common.py +114 -0
  24. pycontrails/datalib/ecmwf/era5.py +554 -0
  25. pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
  26. pycontrails/datalib/ecmwf/hres.py +804 -0
  27. pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
  28. pycontrails/datalib/ecmwf/ifs.py +287 -0
  29. pycontrails/datalib/ecmwf/model_levels.py +435 -0
  30. pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
  31. pycontrails/datalib/ecmwf/variables.py +268 -0
  32. pycontrails/datalib/geo_utils.py +261 -0
  33. pycontrails/datalib/gfs/__init__.py +28 -0
  34. pycontrails/datalib/gfs/gfs.py +656 -0
  35. pycontrails/datalib/gfs/variables.py +104 -0
  36. pycontrails/datalib/goes.py +757 -0
  37. pycontrails/datalib/himawari/__init__.py +27 -0
  38. pycontrails/datalib/himawari/header_struct.py +266 -0
  39. pycontrails/datalib/himawari/himawari.py +667 -0
  40. pycontrails/datalib/landsat.py +589 -0
  41. pycontrails/datalib/leo_utils/__init__.py +5 -0
  42. pycontrails/datalib/leo_utils/correction.py +266 -0
  43. pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
  44. pycontrails/datalib/leo_utils/search.py +250 -0
  45. pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
  46. pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
  47. pycontrails/datalib/leo_utils/vis.py +59 -0
  48. pycontrails/datalib/sentinel.py +650 -0
  49. pycontrails/datalib/spire/__init__.py +5 -0
  50. pycontrails/datalib/spire/exceptions.py +62 -0
  51. pycontrails/datalib/spire/spire.py +604 -0
  52. pycontrails/ext/bada.py +42 -0
  53. pycontrails/ext/cirium.py +14 -0
  54. pycontrails/ext/empirical_grid.py +140 -0
  55. pycontrails/ext/synthetic_flight.py +431 -0
  56. pycontrails/models/__init__.py +1 -0
  57. pycontrails/models/accf.py +425 -0
  58. pycontrails/models/apcemm/__init__.py +8 -0
  59. pycontrails/models/apcemm/apcemm.py +983 -0
  60. pycontrails/models/apcemm/inputs.py +226 -0
  61. pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
  62. pycontrails/models/apcemm/utils.py +437 -0
  63. pycontrails/models/cocip/__init__.py +29 -0
  64. pycontrails/models/cocip/cocip.py +2742 -0
  65. pycontrails/models/cocip/cocip_params.py +305 -0
  66. pycontrails/models/cocip/cocip_uncertainty.py +291 -0
  67. pycontrails/models/cocip/contrail_properties.py +1530 -0
  68. pycontrails/models/cocip/output_formats.py +2270 -0
  69. pycontrails/models/cocip/radiative_forcing.py +1260 -0
  70. pycontrails/models/cocip/radiative_heating.py +520 -0
  71. pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
  72. pycontrails/models/cocip/wake_vortex.py +396 -0
  73. pycontrails/models/cocip/wind_shear.py +120 -0
  74. pycontrails/models/cocipgrid/__init__.py +9 -0
  75. pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
  76. pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
  77. pycontrails/models/dry_advection.py +602 -0
  78. pycontrails/models/emissions/__init__.py +21 -0
  79. pycontrails/models/emissions/black_carbon.py +599 -0
  80. pycontrails/models/emissions/emissions.py +1353 -0
  81. pycontrails/models/emissions/ffm2.py +336 -0
  82. pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
  83. pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
  84. pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
  85. pycontrails/models/extended_k15.py +1327 -0
  86. pycontrails/models/humidity_scaling/__init__.py +37 -0
  87. pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
  88. pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
  89. pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
  90. pycontrails/models/issr.py +210 -0
  91. pycontrails/models/pcc.py +326 -0
  92. pycontrails/models/pcr.py +154 -0
  93. pycontrails/models/ps_model/__init__.py +18 -0
  94. pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
  95. pycontrails/models/ps_model/ps_grid.py +701 -0
  96. pycontrails/models/ps_model/ps_model.py +1000 -0
  97. pycontrails/models/ps_model/ps_operational_limits.py +525 -0
  98. pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
  99. pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
  100. pycontrails/models/sac.py +442 -0
  101. pycontrails/models/tau_cirrus.py +183 -0
  102. pycontrails/physics/__init__.py +1 -0
  103. pycontrails/physics/constants.py +117 -0
  104. pycontrails/physics/geo.py +1138 -0
  105. pycontrails/physics/jet.py +968 -0
  106. pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
  107. pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
  108. pycontrails/physics/thermo.py +551 -0
  109. pycontrails/physics/units.py +472 -0
  110. pycontrails/py.typed +0 -0
  111. pycontrails/utils/__init__.py +1 -0
  112. pycontrails/utils/dependencies.py +66 -0
  113. pycontrails/utils/iteration.py +13 -0
  114. pycontrails/utils/json.py +187 -0
  115. pycontrails/utils/temp.py +50 -0
  116. pycontrails/utils/types.py +163 -0
  117. pycontrails-0.58.0.dist-info/METADATA +180 -0
  118. pycontrails-0.58.0.dist-info/RECORD +122 -0
  119. pycontrails-0.58.0.dist-info/WHEEL +6 -0
  120. pycontrails-0.58.0.dist-info/licenses/LICENSE +178 -0
  121. pycontrails-0.58.0.dist-info/licenses/NOTICE +43 -0
  122. pycontrails-0.58.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,701 @@
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
+
14
+ from pycontrails.core.aircraft_performance import (
15
+ AircraftPerformanceGrid,
16
+ AircraftPerformanceGridData,
17
+ AircraftPerformanceGridParams,
18
+ )
19
+ from pycontrails.core.flight import Flight
20
+ from pycontrails.core.fuel import JetA
21
+ from pycontrails.core.met import MetDataset
22
+ from pycontrails.core.met_var import AirTemperature, MetVariable
23
+ from pycontrails.core.vector import GeoVectorDataset
24
+ from pycontrails.models.ps_model import ps_model, ps_operational_limits
25
+ from pycontrails.models.ps_model.ps_aircraft_params import PSAircraftEngineParams
26
+ from pycontrails.physics import units
27
+ from pycontrails.utils.types import ArrayOrFloat
28
+
29
+ # mypy: disable-error-code = type-var
30
+
31
+
32
+ @dataclasses.dataclass
33
+ class PSGridParams(AircraftPerformanceGridParams):
34
+ """Parameters for :class:`PSGrid`."""
35
+
36
+ #: Passed into :func:`ps_nominal_grid`
37
+ maxiter: int = 10
38
+
39
+
40
+ class PSGrid(AircraftPerformanceGrid):
41
+ """Compute nominal Poll-Schumann aircraft performance over a grid.
42
+
43
+ For a given aircraft type, altitude, aircraft mass, air temperature, and
44
+ mach number, the PS model computes a theoretical engine efficiency and fuel
45
+ flow rate for an aircraft under cruise conditions. Letting the aircraft mass
46
+ vary and fixing the other parameters, the engine efficiency curve attains a
47
+ single maximum at a particular aircraft mass. By solving this implicit
48
+ equation, the PS model can be used to compute the aircraft mass that
49
+ maximizes engine efficiency for a given set of parameters. This is the
50
+ "nominal" aircraft mass computed by this model.
51
+
52
+ This nominal aircraft mass is not always realizable. For example, the maximum
53
+ engine efficiency may be attained at an aircraft mass that is less than the
54
+ operating empty mass of the aircraft. This model determines the minimum and
55
+ maximum possible aircraft mass for a given set of parameters using a simple
56
+ heuristic. The nominal aircraft mass is then clipped to this range.
57
+ """
58
+
59
+ name = "PSGrid"
60
+ long_name = "Poll-Schumann Aircraft Performance evaluated at arbitrary points"
61
+ met_variables: tuple[MetVariable, ...] = (AirTemperature,)
62
+ default_params = PSGridParams
63
+
64
+ met: MetDataset
65
+ source: GeoVectorDataset
66
+
67
+ @overload
68
+ def eval(self, source: GeoVectorDataset, **params: Any) -> GeoVectorDataset: ...
69
+
70
+ @overload
71
+ def eval(self, source: MetDataset | None = ..., **params: Any) -> MetDataset: ...
72
+
73
+ def eval(
74
+ self, source: GeoVectorDataset | MetDataset | None = None, **params: Any
75
+ ) -> GeoVectorDataset | MetDataset:
76
+ """Evaluate the PS model over a :class:`MetDataset` or :class:`GeoVectorDataset`.
77
+
78
+ Parameters
79
+ ----------
80
+ source : GeoVectorDataset | MetDataset | None, optional
81
+ The source data to use for the evaluation. If None, the source is taken
82
+ from the :attr:`met` attribute of the :class:`PSGrid` instance.
83
+ The aircraft type is taken from ``source.attrs["aircraft_type"]``. If this field
84
+ is not present, ``params["aircraft_type"]`` is used instead. See the
85
+ static CSV file :file:`ps-aircraft-params-20240524.csv` for a list of supported
86
+ aircraft types.
87
+ **params : Any
88
+ Override the default parameters of the :class:`PSGrid` instance.
89
+
90
+ Returns
91
+ -------
92
+ GeoVectorDataset | MetDataset
93
+ The source data with the following variables added:
94
+
95
+ - aircraft_mass
96
+ - fuel_flow
97
+ - engine_efficiency
98
+
99
+ Raises
100
+ ------
101
+ NotImplementedError
102
+ If "true_airspeed" or "aircraft_mass" fields are included in
103
+ :attr:`source`.
104
+
105
+ See Also
106
+ --------
107
+ :func:`ps_nominal_grid`
108
+ """
109
+ self.update_params(**params)
110
+ self.set_source(source)
111
+ self.require_source_type((GeoVectorDataset, MetDataset))
112
+ self.set_source_met()
113
+
114
+ # Check some assumptions
115
+ if "true_airspeed" in self.source or "true_airspeed" in self.source.attrs:
116
+ msg = "PSGrid currently only supports setting a 'mach_number' parameter."
117
+ raise NotImplementedError(msg)
118
+ if self.get_source_param("aircraft_mass", set_attr=False) is not None:
119
+ msg = "The 'aircraft_mass' parameter must be None."
120
+ raise NotImplementedError(msg)
121
+ if isinstance(self.source, Flight):
122
+ warnings.warn(
123
+ "The 'PSGrid' model is not intended to support 'Flight' objects as 'source' "
124
+ "data. Instead, use the 'PSFlight' model."
125
+ )
126
+
127
+ # Extract the relevant source data
128
+ try:
129
+ aircraft_type = self.source.attrs["aircraft_type"]
130
+ except KeyError:
131
+ aircraft_type = self.params["aircraft_type"]
132
+ self.source.attrs["aircraft_type"] = aircraft_type
133
+
134
+ fuel = self.source.attrs.get("fuel", self.params["fuel"])
135
+ q_fuel = fuel.q_fuel
136
+ mach_number = self.get_source_param("mach_number", set_attr=False)
137
+
138
+ if isinstance(self.source, MetDataset):
139
+ if "fuel_flow" in self.source:
140
+ msg = "PSGrid doesn't support custom 'fuel_flow' values."
141
+ raise NotImplementedError(msg)
142
+ if "engine_efficiency" in self.source:
143
+ msg = "PSGrid doesn't support custom 'engine_efficiency' values."
144
+ raise NotImplementedError(msg)
145
+
146
+ ds = ps_nominal_grid(
147
+ aircraft_type,
148
+ air_temperature=self.source.data["air_temperature"],
149
+ q_fuel=q_fuel,
150
+ mach_number=mach_number,
151
+ maxiter=self.params["maxiter"],
152
+ )
153
+ return MetDataset(ds)
154
+
155
+ air_temperature = self.source["air_temperature"]
156
+ ds = ps_nominal_grid(
157
+ aircraft_type,
158
+ level=self.source.level,
159
+ air_temperature=air_temperature,
160
+ q_fuel=q_fuel,
161
+ mach_number=mach_number,
162
+ maxiter=self.params["maxiter"],
163
+ )
164
+
165
+ # Set the source data
166
+ self.source.setdefault("aircraft_mass", ds["aircraft_mass"])
167
+ self.source.setdefault("fuel_flow", ds["fuel_flow"])
168
+ self.source.setdefault("engine_efficiency", ds["engine_efficiency"])
169
+ mach_number = self.source.attrs.setdefault("mach_number", ds.attrs["mach_number"])
170
+ self.source["true_airspeed"] = units.mach_number_to_tas(mach_number, air_temperature)
171
+ self.source.attrs.setdefault("wingspan", ds.attrs["wingspan"])
172
+ self.source.attrs.setdefault("n_engine", ds.attrs["n_engine"])
173
+
174
+ return self.source
175
+
176
+
177
+ @dataclasses.dataclass
178
+ class _PerfVariables:
179
+ atyp_param: PSAircraftEngineParams
180
+ air_pressure: npt.NDArray[np.floating] | float
181
+ air_temperature: npt.NDArray[np.floating] | float
182
+ mach_number: npt.NDArray[np.floating] | float
183
+ q_fuel: float
184
+
185
+
186
+ def _nominal_perf(aircraft_mass: ArrayOrFloat, perf: _PerfVariables) -> AircraftPerformanceGridData:
187
+ """Compute nominal Poll-Schumann aircraft performance."""
188
+
189
+ atyp_param = perf.atyp_param
190
+ air_pressure = perf.air_pressure
191
+ air_temperature = perf.air_temperature
192
+ mach_number = perf.mach_number
193
+ q_fuel = perf.q_fuel
194
+
195
+ # Using np.float32 here avoids scalar promotion to float64 via numpy 2.0 and NEP50
196
+ # In other words, the dtype of the perf variables is maintained
197
+ theta = np.float32(0.0)
198
+ dv_dt = np.float32(0.0)
199
+
200
+ rn = ps_model.reynolds_number(
201
+ atyp_param.wing_surface_area, mach_number, air_temperature, air_pressure
202
+ )
203
+
204
+ c_lift = ps_model.lift_coefficient(
205
+ atyp_param.wing_surface_area, aircraft_mass, air_pressure, mach_number, theta
206
+ )
207
+ c_f = ps_model.skin_friction_coefficient(rn)
208
+ c_drag_0 = ps_model.zero_lift_drag_coefficient(c_f, atyp_param.psi_0)
209
+ e_ls = ps_model.oswald_efficiency_factor(c_drag_0, atyp_param)
210
+ c_drag_w = ps_model.wave_drag_coefficient(mach_number, c_lift, atyp_param)
211
+ c_drag = ps_model.airframe_drag_coefficient(
212
+ c_drag_0, c_drag_w, c_lift, e_ls, atyp_param.wing_aspect_ratio
213
+ )
214
+
215
+ thrust = ps_model.thrust_force(aircraft_mass, c_lift, c_drag, dv_dt, theta)
216
+
217
+ c_t = ps_model.engine_thrust_coefficient(
218
+ thrust, mach_number, air_pressure, atyp_param.wing_surface_area
219
+ )
220
+ c_t_eta_b = ps_model.thrust_coefficient_at_max_efficiency(
221
+ mach_number, atyp_param.m_des, atyp_param.c_t_des
222
+ )
223
+
224
+ # Always correct thrust coefficients in the gridded case
225
+ # (In the flight case, this correction is governed by a model parameter)
226
+ c_t_available = ps_operational_limits.max_available_thrust_coefficient(
227
+ air_temperature, mach_number, c_t_eta_b, atyp_param
228
+ )
229
+ np.clip(c_t, 0.0, c_t_available, out=c_t)
230
+
231
+ engine_efficiency = ps_model.overall_propulsion_efficiency(
232
+ mach_number, c_t, c_t_eta_b, atyp_param
233
+ )
234
+
235
+ fuel_flow = ps_model.fuel_mass_flow_rate(
236
+ air_pressure,
237
+ air_temperature,
238
+ mach_number,
239
+ c_t,
240
+ engine_efficiency,
241
+ atyp_param.wing_surface_area,
242
+ q_fuel,
243
+ )
244
+
245
+ return AircraftPerformanceGridData(
246
+ fuel_flow=fuel_flow,
247
+ engine_efficiency=engine_efficiency,
248
+ )
249
+
250
+
251
+ def _newton_func(aircraft_mass: ArrayOrFloat, perf: _PerfVariables) -> ArrayOrFloat:
252
+ """Approximate the derivative of the engine efficiency with respect to mass.
253
+
254
+ This is used to find the mass at which the engine efficiency is maximized.
255
+ """
256
+ eta1 = _nominal_perf(aircraft_mass + 0.5, perf).engine_efficiency
257
+ eta2 = _nominal_perf(aircraft_mass - 0.5, perf).engine_efficiency
258
+ return eta1 - eta2
259
+
260
+
261
+ def _min_mass(
262
+ oem: float,
263
+ lf: float,
264
+ mpm: float,
265
+ reserve_fuel: float,
266
+ ) -> float:
267
+ """Calculate the minimum mass given OEM, LF, MPM, and reserve fuel."""
268
+ return oem + lf * mpm + reserve_fuel
269
+
270
+
271
+ def _estimate_mass_extremes(
272
+ atyp_param: PSAircraftEngineParams,
273
+ perf: _PerfVariables,
274
+ n_iter: int = 3,
275
+ ) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]:
276
+ """Calculate the minimum and maximum mass for a given aircraft type."""
277
+
278
+ oem = atyp_param.amass_oew # operating empty mass
279
+ lf = 0.7 # load factor
280
+ mpm = atyp_param.amass_mpl # max payload mass
281
+ mtow = atyp_param.amass_mtow # max takeoff mass
282
+
283
+ min_mass = _min_mass(oem, lf, mpm, 0.0) # no reserve fuel
284
+ for _ in range(n_iter):
285
+ # Estimate the fuel required to cruise at 35,000 ft for 90 minutes.
286
+ # This is used to compute the reserve fuel.
287
+ ff = _nominal_perf(min_mass, perf).fuel_flow
288
+ reserve_fuel = ff * 60.0 * 90.0 # 90 minutes
289
+ min_mass = _min_mass(oem, lf, mpm, reserve_fuel)
290
+
291
+ # Crude: Assume 2x the fuel flow of cruise for climb
292
+ # Compute the maximum weight at cruise by assuming a 20 minute climb
293
+ ff = _nominal_perf(mtow, perf).fuel_flow
294
+ max_mass = mtow - 2.0 * ff * 60.0 * 20.0
295
+
296
+ return min_mass, max_mass # type: ignore[return-value]
297
+
298
+
299
+ def _parse_variables(
300
+ level: npt.NDArray[np.floating] | None,
301
+ air_temperature: xr.DataArray | npt.NDArray[np.floating] | None,
302
+ ) -> tuple[
303
+ tuple[str],
304
+ dict[str, npt.NDArray[np.floating]],
305
+ npt.NDArray[np.floating],
306
+ npt.NDArray[np.floating],
307
+ ]:
308
+ """Parse the level and air temperature arguments.
309
+
310
+ Returns a tuple of ``(dims, coords, air_pressure, air_temperature)``.
311
+ """
312
+ if isinstance(air_temperature, xr.DataArray):
313
+ if level is not None:
314
+ msg = "If 'air_temperature' is a DataArray, 'level' must be None"
315
+ raise ValueError(msg)
316
+
317
+ try:
318
+ pressure_da = air_temperature["air_pressure"]
319
+ except KeyError as exc:
320
+ msg = "An 'air_pressure' coordinate must be present in 'air_temperature'"
321
+ raise KeyError(msg) from exc
322
+
323
+ air_temperature, pressure_da = xr.broadcast(air_temperature, pressure_da)
324
+ return ( # type: ignore[return-value]
325
+ air_temperature.dims,
326
+ air_temperature.coords,
327
+ np.asarray(pressure_da),
328
+ np.asarray(air_temperature),
329
+ )
330
+
331
+ if level is None:
332
+ msg = "The 'level' argument must be provided"
333
+ raise ValueError(msg)
334
+
335
+ air_pressure = level * 100.0
336
+ if air_temperature is None:
337
+ altitude_m = units.pl_to_m(level)
338
+ air_temperature = units.m_to_T_isa(altitude_m)
339
+ return ("level",), {"level": level}, air_pressure, air_temperature
340
+
341
+
342
+ def ps_nominal_grid(
343
+ aircraft_type: str,
344
+ *,
345
+ level: npt.NDArray[np.floating] | None = None,
346
+ air_temperature: xr.DataArray | npt.NDArray[np.floating] | None = None,
347
+ q_fuel: float = JetA.q_fuel,
348
+ mach_number: float | None = None,
349
+ maxiter: int = PSGridParams.maxiter,
350
+ engine_deterioration_factor: float = PSGridParams.engine_deterioration_factor,
351
+ ) -> xr.Dataset:
352
+ """Calculate the nominal performance grid for a given aircraft type.
353
+
354
+ This function is similar to the :class:`PSGrid` model, but it doesn't require
355
+ meteorological data. Instead, the ambient air temperature can be computed from
356
+ the ISA model or passed as an argument.
357
+
358
+ Parameters
359
+ ----------
360
+ aircraft_type : str
361
+ The aircraft type.
362
+ level : npt.NDArray[np.floating] | None, optional
363
+ The pressure level, [:math:`hPa`]. If None, the ``air_temperature``
364
+ argument must be a :class:`xarray.DataArray` with an ``air_pressure`` coordinate.
365
+ air_temperature : xr.DataArray | npt.NDArray[np.floating] | None, optional
366
+ The ambient air temperature, [:math:`K`]. If None (default), the ISA
367
+ temperature is computed from the ``level`` argument. If a :class:`xarray.DataArray`,
368
+ an ``air_pressure`` coordinate must be present and the ``level`` argument must be None
369
+ to avoid ambiguity. If a :class:`numpy.ndarray` is passed, it is assumed to be 1
370
+ dimensional with the same shape as the ``level`` argument.
371
+ q_fuel : float, optional
372
+ The fuel heating value, by default :attr:`JetA.q_fuel`
373
+ mach_number : float | None, optional
374
+ The Mach number. If None (default), the PS design Mach number is used.
375
+ maxiter : int, optional
376
+ Passed into :func:`scipy.optimize.newton`.
377
+ engine_deterioration_factor : float, optional
378
+ The engine deterioration factor,
379
+ by default :attr:`PSGridParams.engine_deterioration_factor`.
380
+
381
+ Returns
382
+ -------
383
+ xr.Dataset
384
+ The nominal performance grid. The grid is indexed by altitude and Mach number.
385
+ Contains the following variables:
386
+
387
+ - ``"fuel_flow"`` : Fuel flow rate, [:math:`kg/s`]
388
+ - ``"engine_efficiency"`` : Engine efficiency
389
+ - ``"aircraft_mass"`` : Aircraft mass at which the engine efficiency is maximized,
390
+ [:math:`kg`]
391
+
392
+ Raises
393
+ ------
394
+ KeyError
395
+ If "aircraft_type" is not supported by the PS model.
396
+
397
+ See Also
398
+ --------
399
+ ps_nominal_optimize_mach
400
+
401
+ Examples
402
+ --------
403
+ >>> level = np.arange(200, 300, 10, dtype=float)
404
+
405
+ >>> # Compute nominal aircraft performance assuming ISA conditions
406
+ >>> # and the design Mach number
407
+ >>> perf = ps_nominal_grid("A320", level=level)
408
+ >>> perf.attrs["mach_number"]
409
+ 0.753
410
+
411
+ >>> perf.to_dataframe().round({"aircraft_mass": 0, "engine_efficiency": 3, "fuel_flow": 3})
412
+ aircraft_mass engine_efficiency fuel_flow
413
+ level
414
+ 200.0 58416.0 0.301 0.576
415
+ 210.0 61618.0 0.301 0.604
416
+ 220.0 64830.0 0.301 0.633
417
+ 230.0 68026.0 0.301 0.663
418
+ 240.0 71188.0 0.301 0.695
419
+ 250.0 71775.0 0.301 0.703
420
+ 260.0 71766.0 0.300 0.708
421
+ 270.0 71752.0 0.300 0.715
422
+ 280.0 71736.0 0.299 0.722
423
+ 290.0 71717.0 0.298 0.730
424
+
425
+ >>> # Now compute it for a higher Mach number
426
+ >>> perf = ps_nominal_grid("A320", level=level, mach_number=0.78)
427
+ >>> perf.to_dataframe().round({"aircraft_mass": 0, "engine_efficiency": 3, "fuel_flow": 3})
428
+ aircraft_mass engine_efficiency fuel_flow
429
+ level
430
+ 200.0 58473.0 0.307 0.601
431
+ 210.0 60626.0 0.307 0.621
432
+ 220.0 63818.0 0.307 0.651
433
+ 230.0 66994.0 0.307 0.682
434
+ 240.0 70130.0 0.307 0.714
435
+ 250.0 71703.0 0.307 0.733
436
+ 260.0 71690.0 0.306 0.739
437
+ 270.0 71673.0 0.306 0.747
438
+ 280.0 71653.0 0.305 0.756
439
+ 290.0 71631.0 0.304 0.766
440
+ """
441
+ dims, coords, air_pressure, air_temperature = _parse_variables(level, air_temperature)
442
+
443
+ aircraft_engine_params = ps_model.load_aircraft_engine_params(engine_deterioration_factor)
444
+
445
+ try:
446
+ atyp_param = aircraft_engine_params[aircraft_type]
447
+ except KeyError as exc:
448
+ msg = (
449
+ f"The aircraft type {aircraft_type} is not currently supported by the PS model. "
450
+ f"Available aircraft types are: {list(aircraft_engine_params)}"
451
+ )
452
+ raise KeyError(msg) from exc
453
+
454
+ mach_number = mach_number or atyp_param.m_des
455
+
456
+ perf = _PerfVariables(
457
+ atyp_param=atyp_param,
458
+ air_pressure=air_pressure,
459
+ air_temperature=air_temperature,
460
+ mach_number=mach_number,
461
+ q_fuel=q_fuel,
462
+ )
463
+
464
+ min_mass, max_mass = _estimate_mass_extremes(atyp_param, perf)
465
+
466
+ mass_allowed = ps_operational_limits.max_allowable_aircraft_mass(
467
+ air_pressure,
468
+ mach_number=mach_number,
469
+ mach_num_des=atyp_param.m_des,
470
+ c_l_do=atyp_param.c_l_do,
471
+ wing_surface_area=atyp_param.wing_surface_area,
472
+ amass_mtow=atyp_param.amass_mtow,
473
+ )
474
+
475
+ min_mass.clip(max=mass_allowed, out=min_mass) # type: ignore[call-overload]
476
+ max_mass.clip(max=mass_allowed, out=max_mass) # type: ignore[call-overload]
477
+
478
+ x0 = np.full_like(air_temperature, (max_mass + min_mass) / 2.0)
479
+
480
+ # Choose aircraft mass to maximize engine efficiency
481
+ # This is the critical step of the calculation
482
+ aircraft_mass = scipy.optimize.newton(
483
+ func=_newton_func,
484
+ args=(perf,),
485
+ x0=x0,
486
+ tol=80.0, # use roughly the weight of a passenger as a tolerance
487
+ disp=False,
488
+ maxiter=maxiter,
489
+ )
490
+
491
+ # scipy.optimize.newton promotes float32 to float64
492
+ aircraft_mass = aircraft_mass.astype(x0.dtype, copy=False)
493
+
494
+ aircraft_mass.clip(min=min_mass, max=max_mass, out=aircraft_mass)
495
+
496
+ output = _nominal_perf(aircraft_mass, perf)
497
+
498
+ engine_efficiency = output.engine_efficiency
499
+ fuel_flow = output.fuel_flow
500
+
501
+ attrs = {
502
+ "aircraft_type": aircraft_type,
503
+ "mach_number": mach_number,
504
+ "q_fuel": q_fuel,
505
+ "wingspan": atyp_param.wing_span,
506
+ "n_engine": atyp_param.n_engine,
507
+ }
508
+
509
+ return xr.Dataset(
510
+ {
511
+ "aircraft_mass": (dims, aircraft_mass),
512
+ "engine_efficiency": (dims, engine_efficiency),
513
+ "fuel_flow": (dims, fuel_flow),
514
+ },
515
+ coords=coords,
516
+ attrs=attrs,
517
+ )
518
+
519
+
520
+ def _newton_mach(
521
+ mach_number: ArrayOrFloat,
522
+ perf: _PerfVariables,
523
+ aircraft_mass: ArrayOrFloat,
524
+ headwind: ArrayOrFloat,
525
+ cost_index: ArrayOrFloat,
526
+ ) -> ArrayOrFloat:
527
+ """Approximate the derivative of the cost of a segment based on mach number.
528
+
529
+ This is used to find the mach number at which cost in minimized.
530
+ """
531
+ perf.mach_number = mach_number + 1e-4
532
+ tas = units.mach_number_to_tas(perf.mach_number, perf.air_temperature)
533
+ groundspeed = tas - headwind
534
+ ff1 = _nominal_perf(aircraft_mass, perf).fuel_flow
535
+ eccf1 = (cost_index + ff1 * 60) / groundspeed
536
+
537
+ perf.mach_number = mach_number - 1e-4
538
+ tas = units.mach_number_to_tas(perf.mach_number, perf.air_temperature)
539
+ groundspeed = tas - headwind
540
+ ff2 = _nominal_perf(aircraft_mass, perf).fuel_flow
541
+ eccf2 = (cost_index + ff2 * 60) / groundspeed
542
+ return eccf1 - eccf2
543
+
544
+
545
+ def ps_nominal_optimize_mach(
546
+ aircraft_type: str,
547
+ aircraft_mass: ArrayOrFloat,
548
+ cost_index: ArrayOrFloat,
549
+ level: ArrayOrFloat,
550
+ *,
551
+ air_temperature: ArrayOrFloat | None = None,
552
+ northward_wind: ArrayOrFloat | None = None,
553
+ eastward_wind: ArrayOrFloat | None = None,
554
+ sin_a: ArrayOrFloat | None = None,
555
+ cos_a: ArrayOrFloat | None = None,
556
+ q_fuel: float = JetA.q_fuel,
557
+ engine_deterioration_factor: float = PSGridParams.engine_deterioration_factor,
558
+ ) -> xr.Dataset:
559
+ """Calculate the nominal optimal mach number for a given aircraft type.
560
+
561
+ This function is similar to the :class:`ps_nominal_grid` method, but rather than
562
+ maximizing engine efficiency by adjusting aircraft, we are minimizing cost by adjusting
563
+ mach number.
564
+
565
+ Parameters
566
+ ----------
567
+ aircraft_type : str
568
+ The aircraft type.
569
+ aircraft_mass: ArrayOrFloat
570
+ The aircraft mass, [:math:`kg`].
571
+ cost_index: ArrayOrFloat
572
+ The cost index, [:math:`kg/min`], or non-fuel cost of one minute of flight time
573
+ level : ArrayOrFloat
574
+ The pressure level, [:math:`hPa`]. If a :class:`numpy.ndarray` is passed, it is
575
+ assumed to be one dimensional and the same length as the``aircraft_mass`` argument.
576
+ air_temperature : ArrayOrFloat | None, optional
577
+ The ambient air temperature, [:math:`K`]. If None (default), the ISA
578
+ temperature is computed from the ``level`` argument. If a :class:`numpy.ndarray`
579
+ is passed, it is assumed to be one dimensional and the same length as the
580
+ ``aircraft_mass`` argument.
581
+ air_temperature : ArrayOrFloat | None, optional
582
+ northward_wind: ArrayOrFloat | None = None, optional
583
+ The northward component of winds, [:math:`m/s`]. If None (default) assumed to be
584
+ zero.
585
+ eastward_wind: ArrayOrFloat | None = None, optional
586
+ The eastward component of winds, [:math:`m/s`]. If None (default) assumed to be
587
+ zero.
588
+ sin_a: ArrayOrFloat | None = None, optional
589
+ The sine between the true bearing of flight and the longitudinal axis. Must be
590
+ specified if wind data is provided. Will be ignored if wind data is not provided.
591
+ cos_a: ArrayOrFloat | None = None, optional
592
+ The cosine between the true bearing of flight and the longitudinal axis. Must be
593
+ specified if wind data is provided. Will be ignored if wind data is not provided.
594
+ q_fuel : float, optional
595
+ The fuel heating value, by default :attr:`JetA.q_fuel`.
596
+ engine_deterioration_factor : float, optional
597
+ The engine deterioration factor,
598
+ by default :attr:`PSGridParams.engine_deterioration_factor`.
599
+
600
+ Returns
601
+ -------
602
+ xr.Dataset
603
+ The nominal performance grid. The grid is indexed by altitude.
604
+ Contains the following variables:
605
+
606
+ - ``"mach_number"``: The mach number that minimizes segment cost
607
+ - ``"fuel_flow"`` : Fuel flow rate, [:math:`kg/s`]
608
+ - ``"engine_efficiency"`` : Engine efficiency
609
+ - ``"aircraft_mass"`` : Aircraft mass, [:math:`kg`]
610
+
611
+ Raises
612
+ ------
613
+ KeyError
614
+ If "aircraft_type" is not supported by the PS model.
615
+ ValueError
616
+ If wind data is provided without segment angles.
617
+
618
+ See Also
619
+ --------
620
+ ps_nominal_grid
621
+ """
622
+ dims = ("level",)
623
+ coords = {"level": level}
624
+ aircraft_engine_params = ps_model.load_aircraft_engine_params(engine_deterioration_factor)
625
+ try:
626
+ atyp_param = aircraft_engine_params[aircraft_type]
627
+ except KeyError as exc:
628
+ msg = (
629
+ f"The aircraft type {aircraft_type} is not currently supported by the PS model. "
630
+ f"Available aircraft types are: {list(aircraft_engine_params)}"
631
+ )
632
+ raise KeyError(msg) from exc
633
+
634
+ if air_temperature is None:
635
+ altitude_m = units.pl_to_m(level)
636
+ air_temperature = units.m_to_T_isa(altitude_m)
637
+
638
+ if northward_wind is not None and eastward_wind is not None:
639
+ if sin_a is None or cos_a is None:
640
+ msg = "Segment angles must be provide if wind data is specified"
641
+ raise ValueError(msg)
642
+ headwind = -(northward_wind * cos_a + eastward_wind * sin_a)
643
+ else:
644
+ headwind = 0.0
645
+
646
+ min_mach = ps_operational_limits.minimum_mach_num(
647
+ air_pressure=level * 100.0,
648
+ aircraft_mass=aircraft_mass,
649
+ atyp_param=atyp_param,
650
+ )
651
+
652
+ max_mach = ps_operational_limits.maximum_mach_num(
653
+ altitude_ft=units.pl_to_ft(level),
654
+ air_pressure=level * 100.0,
655
+ aircraft_mass=aircraft_mass,
656
+ air_temperature=air_temperature,
657
+ theta=np.full_like(aircraft_mass, 0.0),
658
+ atyp_param=atyp_param,
659
+ )
660
+
661
+ x0 = (min_mach + max_mach) / 2.0 # type: ignore
662
+
663
+ perf = _PerfVariables(
664
+ atyp_param=atyp_param,
665
+ air_pressure=level * 100.0,
666
+ air_temperature=air_temperature,
667
+ mach_number=x0,
668
+ q_fuel=q_fuel,
669
+ )
670
+
671
+ opt_mach = scipy.optimize.newton(
672
+ func=_newton_mach,
673
+ args=(perf, aircraft_mass, headwind, cost_index),
674
+ x0=x0,
675
+ tol=1e-4,
676
+ disp=False,
677
+ ).clip(min=min_mach, max=max_mach)
678
+
679
+ perf.mach_number = opt_mach
680
+ output = _nominal_perf(aircraft_mass, perf)
681
+
682
+ engine_efficiency = output.engine_efficiency
683
+ fuel_flow = output.fuel_flow
684
+
685
+ attrs = {
686
+ "aircraft_type": aircraft_type,
687
+ "q_fuel": q_fuel,
688
+ "wingspan": atyp_param.wing_span,
689
+ "n_engine": atyp_param.n_engine,
690
+ }
691
+
692
+ return xr.Dataset(
693
+ {
694
+ "mach_number": (dims, opt_mach),
695
+ "aircraft_mass": (dims, aircraft_mass),
696
+ "engine_efficiency": (dims, engine_efficiency),
697
+ "fuel_flow": (dims, fuel_flow),
698
+ },
699
+ coords=coords,
700
+ attrs=attrs,
701
+ )