pycontrails 0.54.2__cp310-cp310-win_amd64.whl → 0.54.4__cp310-cp310-win_amd64.whl

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

Potentially problematic release.


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

Files changed (68) hide show
  1. pycontrails/__init__.py +2 -2
  2. pycontrails/_version.py +2 -2
  3. pycontrails/core/__init__.py +1 -1
  4. pycontrails/core/aircraft_performance.py +75 -61
  5. pycontrails/core/cache.py +7 -7
  6. pycontrails/core/fleet.py +25 -21
  7. pycontrails/core/flight.py +215 -301
  8. pycontrails/core/interpolation.py +56 -56
  9. pycontrails/core/met.py +48 -39
  10. pycontrails/core/models.py +25 -11
  11. pycontrails/core/polygon.py +15 -15
  12. pycontrails/core/rgi_cython.cp310-win_amd64.pyd +0 -0
  13. pycontrails/core/vector.py +22 -22
  14. pycontrails/datalib/_met_utils/metsource.py +8 -5
  15. pycontrails/datalib/ecmwf/__init__.py +14 -14
  16. pycontrails/datalib/ecmwf/common.py +1 -1
  17. pycontrails/datalib/ecmwf/era5.py +7 -7
  18. pycontrails/datalib/ecmwf/hres.py +3 -3
  19. pycontrails/datalib/ecmwf/ifs.py +1 -1
  20. pycontrails/datalib/ecmwf/variables.py +1 -0
  21. pycontrails/datalib/gfs/__init__.py +6 -6
  22. pycontrails/datalib/gfs/gfs.py +2 -2
  23. pycontrails/datalib/goes.py +5 -5
  24. pycontrails/datalib/landsat.py +5 -8
  25. pycontrails/datalib/sentinel.py +7 -11
  26. pycontrails/ext/bada.py +3 -2
  27. pycontrails/ext/empirical_grid.py +1 -1
  28. pycontrails/ext/synthetic_flight.py +3 -2
  29. pycontrails/models/accf.py +40 -19
  30. pycontrails/models/apcemm/apcemm.py +5 -4
  31. pycontrails/models/cocip/__init__.py +2 -2
  32. pycontrails/models/cocip/cocip.py +16 -17
  33. pycontrails/models/cocip/cocip_params.py +2 -11
  34. pycontrails/models/cocip/cocip_uncertainty.py +24 -18
  35. pycontrails/models/cocip/contrail_properties.py +331 -316
  36. pycontrails/models/cocip/output_formats.py +53 -53
  37. pycontrails/models/cocip/radiative_forcing.py +135 -131
  38. pycontrails/models/cocip/radiative_heating.py +135 -135
  39. pycontrails/models/cocip/unterstrasser_wake_vortex.py +90 -87
  40. pycontrails/models/cocip/wake_vortex.py +92 -92
  41. pycontrails/models/cocip/wind_shear.py +8 -8
  42. pycontrails/models/cocipgrid/cocip_grid.py +118 -107
  43. pycontrails/models/dry_advection.py +59 -58
  44. pycontrails/models/emissions/__init__.py +2 -2
  45. pycontrails/models/emissions/black_carbon.py +108 -108
  46. pycontrails/models/emissions/emissions.py +85 -85
  47. pycontrails/models/emissions/ffm2.py +35 -35
  48. pycontrails/models/humidity_scaling/humidity_scaling.py +23 -23
  49. pycontrails/models/ps_model/__init__.py +3 -2
  50. pycontrails/models/ps_model/ps_aircraft_params.py +11 -6
  51. pycontrails/models/ps_model/ps_grid.py +256 -60
  52. pycontrails/models/ps_model/ps_model.py +18 -21
  53. pycontrails/models/ps_model/ps_operational_limits.py +58 -69
  54. pycontrails/models/tau_cirrus.py +8 -1
  55. pycontrails/physics/geo.py +216 -67
  56. pycontrails/physics/jet.py +220 -90
  57. pycontrails/physics/static/iata-cargo-load-factors-20241115.csv +71 -0
  58. pycontrails/physics/static/iata-passenger-load-factors-20241115.csv +71 -0
  59. pycontrails/physics/units.py +14 -14
  60. pycontrails/utils/json.py +1 -2
  61. pycontrails/utils/types.py +12 -7
  62. {pycontrails-0.54.2.dist-info → pycontrails-0.54.4.dist-info}/METADATA +10 -10
  63. {pycontrails-0.54.2.dist-info → pycontrails-0.54.4.dist-info}/NOTICE +1 -1
  64. pycontrails-0.54.4.dist-info/RECORD +111 -0
  65. {pycontrails-0.54.2.dist-info → pycontrails-0.54.4.dist-info}/WHEEL +1 -1
  66. pycontrails-0.54.2.dist-info/RECORD +0 -109
  67. {pycontrails-0.54.2.dist-info → pycontrails-0.54.4.dist-info}/LICENSE +0 -0
  68. {pycontrails-0.54.2.dist-info → pycontrails-0.54.4.dist-info}/top_level.txt +0 -0
@@ -11,7 +11,9 @@ from typing import Any
11
11
  import numpy as np
12
12
  import pandas as pd
13
13
 
14
+ from pycontrails.core.aircraft_performance import AircraftPerformanceParams
14
15
  from pycontrails.physics import constants as c
16
+ from pycontrails.utils.types import ArrayOrFloat
15
17
 
16
18
  #: Path to the Poll-Schumann aircraft parameters CSV file.
17
19
  PS_FILE_PATH = pathlib.Path(__file__).parent / "static" / "ps-aircraft-params-20240524.csv"
@@ -193,7 +195,7 @@ def _row_to_aircraft_engine_params(tup: Any) -> tuple[str, PSAircraftEngineParam
193
195
 
194
196
  @functools.cache
195
197
  def load_aircraft_engine_params(
196
- engine_deterioration_factor: float = 0.025,
198
+ engine_deterioration_factor: float = AircraftPerformanceParams.engine_deterioration_factor,
197
199
  ) -> Mapping[str, PSAircraftEngineParams]:
198
200
  """
199
201
  Extract aircraft-engine parameters for each aircraft type supported by the PS model.
@@ -254,23 +256,23 @@ def load_aircraft_engine_params(
254
256
  }
255
257
 
256
258
  df = pd.read_csv(PS_FILE_PATH, dtype=dtypes)
257
- df["eta_1"] = df["eta_1"] * (1.0 - engine_deterioration_factor)
259
+ df["eta_1"] *= 1.0 - engine_deterioration_factor
258
260
 
259
261
  return dict(_row_to_aircraft_engine_params(tup) for tup in df.itertuples(index=False))
260
262
 
261
263
 
262
- def turbine_entry_temperature_at_max_take_off(first_flight: float) -> float:
264
+ def turbine_entry_temperature_at_max_take_off(first_flight: ArrayOrFloat) -> ArrayOrFloat:
263
265
  """
264
266
  Calculate turbine entry temperature at maximum take-off rating.
265
267
 
266
268
  Parameters
267
269
  ----------
268
- first_flight: float
270
+ first_flight: ArrayOrFloat
269
271
  Year of first flight
270
272
 
271
273
  Returns
272
274
  -------
273
- float
275
+ ArrayOrFloat
274
276
  Turbine entry temperature at maximum take-off rating, ``tet_mto``, [:math:`K`]
275
277
 
276
278
  Notes
@@ -283,7 +285,10 @@ def turbine_entry_temperature_at_max_take_off(first_flight: float) -> float:
283
285
  ----------
284
286
  - :cite:`cumpstyJetPropulsion2015`
285
287
  """
286
- return 2000.0 * (1 - np.exp(62.8 - 0.0325 * first_flight))
288
+ out = 2000.0 * (1.0 - np.exp(62.8 - 0.0325 * first_flight))
289
+ if isinstance(first_flight, np.ndarray):
290
+ return out
291
+ return out.item()
287
292
 
288
293
 
289
294
  def turbine_entry_temperature_at_max_continuous_climb(tet_mto: float) -> float:
@@ -10,7 +10,6 @@ import numpy as np
10
10
  import numpy.typing as npt
11
11
  import scipy.optimize
12
12
  import xarray as xr
13
- import xarray.core.coordinates as xrcc
14
13
 
15
14
  from pycontrails.core.aircraft_performance import (
16
15
  AircraftPerformanceGrid,
@@ -178,9 +177,9 @@ class PSGrid(AircraftPerformanceGrid):
178
177
  @dataclasses.dataclass
179
178
  class _PerfVariables:
180
179
  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
180
+ air_pressure: npt.NDArray[np.floating] | float
181
+ air_temperature: npt.NDArray[np.floating] | float
182
+ mach_number: npt.NDArray[np.floating] | float
184
183
  q_fuel: float
185
184
 
186
185
 
@@ -193,8 +192,10 @@ def _nominal_perf(aircraft_mass: ArrayOrFloat, perf: _PerfVariables) -> Aircraft
193
192
  mach_number = perf.mach_number
194
193
  q_fuel = perf.q_fuel
195
194
 
196
- theta = 0.0
197
- dv_dt = 0.0
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)
198
199
 
199
200
  rn = ps_model.reynolds_number(
200
201
  atyp_param.wing_surface_area, mach_number, air_temperature, air_pressure
@@ -271,7 +272,7 @@ def _estimate_mass_extremes(
271
272
  atyp_param: PSAircraftEngineParams,
272
273
  perf: _PerfVariables,
273
274
  n_iter: int = 3,
274
- ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
275
+ ) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]:
275
276
  """Calculate the minimum and maximum mass for a given aircraft type."""
276
277
 
277
278
  oem = atyp_param.amass_oew # operating empty mass
@@ -296,43 +297,57 @@ def _estimate_mass_extremes(
296
297
 
297
298
 
298
299
  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
-
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
+ """
304
312
  if isinstance(air_temperature, xr.DataArray):
305
313
  if level is not None:
306
314
  msg = "If 'air_temperature' is a DataArray, 'level' must be None"
307
315
  raise ValueError(msg)
308
316
 
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
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
+ )
320
330
 
321
331
  if level is None:
322
- msg = "The 'level' argument must be specified"
332
+ msg = "The 'level' argument must be provided"
323
333
  raise ValueError(msg)
324
334
 
325
- return level, air_temperature
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
326
340
 
327
341
 
328
342
  def ps_nominal_grid(
329
343
  aircraft_type: str,
330
344
  *,
331
- level: npt.NDArray[np.float64] | None = None,
332
- air_temperature: xr.DataArray | npt.NDArray[np.float64] | None = None,
345
+ level: npt.NDArray[np.floating] | None = None,
346
+ air_temperature: xr.DataArray | npt.NDArray[np.floating] | None = None,
333
347
  q_fuel: float = JetA.q_fuel,
334
348
  mach_number: float | None = None,
335
349
  maxiter: int = PSGridParams.maxiter,
350
+ engine_deterioration_factor: float = PSGridParams.engine_deterioration_factor,
336
351
  ) -> xr.Dataset:
337
352
  """Calculate the nominal performance grid for a given aircraft type.
338
353
 
@@ -344,13 +359,13 @@ def ps_nominal_grid(
344
359
  ----------
345
360
  aircraft_type : str
346
361
  The aircraft type.
347
- level : npt.NDArray[np.float64] | None, optional
362
+ level : npt.NDArray[np.floating] | None, optional
348
363
  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
364
+ argument must be a :class:`xarray.DataArray` with an ``air_pressure`` coordinate.
365
+ air_temperature : xr.DataArray | npt.NDArray[np.floating] | None, optional
351
366
  The ambient air temperature, [:math:`K`]. If None (default), the ISA
352
367
  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
368
+ an ``air_pressure`` coordinate must be present and the ``level`` argument must be None
354
369
  to avoid ambiguity. If a :class:`numpy.ndarray` is passed, it is assumed to be 1
355
370
  dimensional with the same shape as the ``level`` argument.
356
371
  q_fuel : float, optional
@@ -359,6 +374,9 @@ def ps_nominal_grid(
359
374
  The Mach number. If None (default), the PS design Mach number is used.
360
375
  maxiter : int, optional
361
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`.
362
380
 
363
381
  Returns
364
382
  -------
@@ -376,6 +394,10 @@ def ps_nominal_grid(
376
394
  KeyError
377
395
  If "aircraft_type" is not supported by the PS model.
378
396
 
397
+ See Also
398
+ --------
399
+ ps_nominal_optimize_mach
400
+
379
401
  Examples
380
402
  --------
381
403
  >>> level = np.arange(200, 300, 10, dtype=float)
@@ -389,16 +411,16 @@ def ps_nominal_grid(
389
411
  >>> perf.to_dataframe()
390
412
  aircraft_mass engine_efficiency fuel_flow
391
413
  level
392
- 200.0 58416.230843 0.300958 0.575635
414
+ 200.0 58416.230844 0.300958 0.575635
393
415
  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
416
+ 220.0 64829.702584 0.300958 0.633199
417
+ 230.0 68026.415694 0.300958 0.662998
396
418
  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
419
+ 250.0 71775.399880 0.300824 0.703349
420
+ 260.0 71765.716789 0.300363 0.708259
421
+ 270.0 71752.405449 0.299671 0.714514
422
+ 280.0 71736.129125 0.298823 0.721878
423
+ 290.0 71717.392213 0.297875 0.730169
402
424
 
403
425
  >>> # Now compute it for a higher Mach number
404
426
  >>> perf = ps_nominal_grid("A320", level=level, mach_number=0.78)
@@ -407,28 +429,18 @@ def ps_nominal_grid(
407
429
  level
408
430
  200.0 57941.825236 0.306598 0.596100
409
431
  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
432
+ 220.0 63818.498305 0.306605 0.650918
433
+ 230.0 66993.691515 0.306605 0.681551
434
+ 240.0 70129.930502 0.306605 0.714069
435
+ 250.0 71703.009114 0.306560 0.732944
436
+ 260.0 71690.188703 0.306239 0.739276
437
+ 270.0 71673.392137 0.305694 0.747052
438
+ 280.0 71653.431366 0.304997 0.755990
439
+ 290.0 71630.901358 0.304201 0.765883
418
440
  """
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}
441
+ dims, coords, air_pressure, air_temperature = _parse_variables(level, air_temperature)
426
442
 
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()
443
+ aircraft_engine_params = ps_model.load_aircraft_engine_params(engine_deterioration_factor)
432
444
 
433
445
  try:
434
446
  atyp_param = aircraft_engine_params[aircraft_type]
@@ -471,7 +483,7 @@ def ps_nominal_grid(
471
483
  func=_newton_func,
472
484
  args=(perf,),
473
485
  x0=x0,
474
- tol=1.0,
486
+ tol=80.0, # use roughly the weight of a passenger as a tolerance
475
487
  disp=False,
476
488
  maxiter=maxiter,
477
489
  )
@@ -503,3 +515,187 @@ def ps_nominal_grid(
503
515
  coords=coords,
504
516
  attrs=attrs,
505
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) # type: ignore[misc]
643
+ else:
644
+ headwind = 0.0 # type: ignore
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
+ )
@@ -51,13 +51,6 @@ class PSFlightParams(AircraftPerformanceParams):
51
51
  #: efficiency to always exceed this value.
52
52
  eta_over_eta_b_min: float | None = 0.5
53
53
 
54
- #: Account for "in-service" engine deterioration between maintenance cycles.
55
- #: Default value is set to +2.5% increase in fuel consumption.
56
- # Reference:
57
- # Gurrola Arrieta, M.D.J., Botez, R.M. and Lasne, A., 2024. An Engine Deterioration Model for
58
- # Predicting Fuel Consumption Impact in a Regional Aircraft. Aerospace, 11(6), p.426.
59
- engine_deterioration_factor: float = 0.025
60
-
61
54
 
62
55
  class PSFlight(AircraftPerformance):
63
56
  """Simulate aircraft performance using Poll-Schumann (PS) model.
@@ -70,6 +63,10 @@ class PSFlight(AircraftPerformance):
70
63
  Poll & Schumann (2022). An estimation method for the fuel burn and other performance
71
64
  characteristics of civil transport aircraft. Part 3 Generalisation to cover climb,
72
65
  descent and holding. Aero. J., submitted.
66
+
67
+ See Also
68
+ --------
69
+ pycontrails.physics.jet.aircraft_load_factor
73
70
  """
74
71
 
75
72
  name = "PSFlight"
@@ -227,14 +224,14 @@ class PSFlight(AircraftPerformance):
227
224
  self,
228
225
  *,
229
226
  aircraft_type: str,
230
- altitude_ft: npt.NDArray[np.float64],
231
- air_temperature: npt.NDArray[np.float64],
227
+ altitude_ft: npt.NDArray[np.floating],
228
+ air_temperature: npt.NDArray[np.floating],
232
229
  time: npt.NDArray[np.datetime64] | None,
233
- true_airspeed: npt.NDArray[np.float64] | float | None,
234
- aircraft_mass: npt.NDArray[np.float64] | float,
235
- engine_efficiency: npt.NDArray[np.float64] | float | None,
236
- fuel_flow: npt.NDArray[np.float64] | float | None,
237
- thrust: npt.NDArray[np.float64] | float | None,
230
+ true_airspeed: npt.NDArray[np.floating] | float | None,
231
+ aircraft_mass: npt.NDArray[np.floating] | float,
232
+ engine_efficiency: npt.NDArray[np.floating] | float | None,
233
+ fuel_flow: npt.NDArray[np.floating] | float | None,
234
+ thrust: npt.NDArray[np.floating] | float | None,
238
235
  q_fuel: float,
239
236
  **kwargs: Any,
240
237
  ) -> AircraftPerformanceData:
@@ -269,8 +266,8 @@ class PSFlight(AircraftPerformance):
269
266
  rn = reynolds_number(atyp_param.wing_surface_area, mach_num, air_temperature, air_pressure)
270
267
 
271
268
  # Allow array or None time
272
- dv_dt: npt.NDArray[np.float64] | float
273
- theta: npt.NDArray[np.float64] | float
269
+ dv_dt: npt.NDArray[np.floating] | float
270
+ theta: npt.NDArray[np.floating] | float
274
271
  if time is None:
275
272
  # Assume a nominal cruising state
276
273
  dt_sec = None
@@ -783,7 +780,7 @@ def overall_propulsion_efficiency(
783
780
  c_t_eta_b: ArrayOrFloat,
784
781
  atyp_param: PSAircraftEngineParams,
785
782
  eta_over_eta_b_min: float | None = None,
786
- ) -> npt.NDArray[np.float64]:
783
+ ) -> npt.NDArray[np.floating]:
787
784
  """Calculate overall propulsion efficiency.
788
785
 
789
786
  Parameters
@@ -803,7 +800,7 @@ def overall_propulsion_efficiency(
803
800
 
804
801
  Returns
805
802
  -------
806
- npt.NDArray[np.float64]
803
+ npt.NDArray[np.floating]
807
804
  Overall propulsion efficiency
808
805
  """
809
806
  eta_over_eta_b = propulsion_efficiency_over_max_propulsion_efficiency(mach_num, c_t, c_t_eta_b)
@@ -819,7 +816,7 @@ def propulsion_efficiency_over_max_propulsion_efficiency(
819
816
  mach_num: ArrayOrFloat,
820
817
  c_t: ArrayOrFloat,
821
818
  c_t_eta_b: ArrayOrFloat,
822
- ) -> npt.NDArray[np.float64]:
819
+ ) -> npt.NDArray[np.floating]:
823
820
  """Calculate ratio of OPE to maximum OPE that can be attained for a given Mach number.
824
821
 
825
822
  Parameters
@@ -833,7 +830,7 @@ def propulsion_efficiency_over_max_propulsion_efficiency(
833
830
 
834
831
  Returns
835
832
  -------
836
- npt.NDArray[np.float64]
833
+ npt.NDArray[np.floating]
837
834
  Ratio of OPE to maximum OPE, ``eta / eta_b``
838
835
 
839
836
  Notes
@@ -843,7 +840,7 @@ def propulsion_efficiency_over_max_propulsion_efficiency(
843
840
  """
844
841
  c_t_over_c_t_eta_b = c_t / c_t_eta_b
845
842
 
846
- sigma = np.where(mach_num < 0.4, 1.3 * (0.4 - mach_num), 0.0)
843
+ sigma = np.where(mach_num < 0.4, 1.3 * (0.4 - mach_num), np.float32(0.0)) # avoid promotion
847
844
 
848
845
  eta_over_eta_b_low = (
849
846
  10.0 * (1.0 + 0.8 * (sigma - 0.43) - 0.6027 * sigma * 0.43) * c_t_over_c_t_eta_b