pycontrails 0.54.1__cp313-cp313-win_amd64.whl → 0.54.3__cp313-cp313-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 (43) hide show
  1. pycontrails/_version.py +2 -2
  2. pycontrails/core/aircraft_performance.py +24 -5
  3. pycontrails/core/cache.py +14 -10
  4. pycontrails/core/fleet.py +22 -12
  5. pycontrails/core/flight.py +25 -15
  6. pycontrails/core/met.py +34 -22
  7. pycontrails/core/rgi_cython.cp313-win_amd64.pyd +0 -0
  8. pycontrails/core/vector.py +38 -38
  9. pycontrails/datalib/ecmwf/arco_era5.py +10 -5
  10. pycontrails/datalib/ecmwf/common.py +7 -2
  11. pycontrails/datalib/ecmwf/era5.py +9 -4
  12. pycontrails/datalib/ecmwf/era5_model_level.py +9 -5
  13. pycontrails/datalib/ecmwf/hres.py +12 -7
  14. pycontrails/datalib/ecmwf/hres_model_level.py +10 -5
  15. pycontrails/datalib/ecmwf/ifs.py +11 -6
  16. pycontrails/datalib/ecmwf/variables.py +1 -0
  17. pycontrails/datalib/gfs/gfs.py +52 -34
  18. pycontrails/datalib/gfs/variables.py +6 -2
  19. pycontrails/datalib/landsat.py +5 -8
  20. pycontrails/datalib/sentinel.py +7 -11
  21. pycontrails/ext/bada.py +3 -2
  22. pycontrails/ext/synthetic_flight.py +3 -2
  23. pycontrails/models/accf.py +40 -19
  24. pycontrails/models/apcemm/apcemm.py +2 -1
  25. pycontrails/models/cocip/cocip.py +8 -4
  26. pycontrails/models/cocipgrid/cocip_grid.py +25 -20
  27. pycontrails/models/dry_advection.py +50 -54
  28. pycontrails/models/humidity_scaling/humidity_scaling.py +12 -7
  29. pycontrails/models/ps_model/__init__.py +2 -1
  30. pycontrails/models/ps_model/ps_aircraft_params.py +3 -2
  31. pycontrails/models/ps_model/ps_grid.py +187 -1
  32. pycontrails/models/ps_model/ps_model.py +12 -10
  33. pycontrails/models/ps_model/ps_operational_limits.py +39 -52
  34. pycontrails/physics/geo.py +149 -0
  35. pycontrails/physics/jet.py +141 -11
  36. pycontrails/physics/static/iata-cargo-load-factors-20241115.csv +71 -0
  37. pycontrails/physics/static/iata-passenger-load-factors-20241115.csv +71 -0
  38. {pycontrails-0.54.1.dist-info → pycontrails-0.54.3.dist-info}/METADATA +12 -11
  39. {pycontrails-0.54.1.dist-info → pycontrails-0.54.3.dist-info}/RECORD +43 -41
  40. {pycontrails-0.54.1.dist-info → pycontrails-0.54.3.dist-info}/WHEEL +1 -1
  41. {pycontrails-0.54.1.dist-info → pycontrails-0.54.3.dist-info}/LICENSE +0 -0
  42. {pycontrails-0.54.1.dist-info → pycontrails-0.54.3.dist-info}/NOTICE +0 -0
  43. {pycontrails-0.54.1.dist-info → pycontrails-0.54.3.dist-info}/top_level.txt +0 -0
@@ -9,7 +9,6 @@ import numpy as np
9
9
  import numpy.typing as npt
10
10
 
11
11
  from pycontrails.core import models
12
- from pycontrails.core.flight import Flight
13
12
  from pycontrails.core.met import MetDataset
14
13
  from pycontrails.core.met_var import AirTemperature, EastwardWind, NorthwardWind, VerticalVelocity
15
14
  from pycontrails.core.vector import GeoVectorDataset
@@ -92,9 +91,6 @@ class DryAdvection(models.Model):
92
91
  met_required = True
93
92
  source: GeoVectorDataset
94
93
 
95
- @overload
96
- def eval(self, source: Flight, **params: Any) -> Flight: ...
97
-
98
94
  @overload
99
95
  def eval(self, source: GeoVectorDataset, **params: Any) -> GeoVectorDataset: ...
100
96
 
@@ -109,7 +105,12 @@ class DryAdvection(models.Model):
109
105
  Parameters
110
106
  ----------
111
107
  source : GeoVectorDataset
112
- Arbitrary points to advect.
108
+ Arbitrary points to advect. A :class:`Flight` instance is not treated any
109
+ differently than a :class:`GeoVectorDataset`. In particular, the user must
110
+ explicitly set ``flight["azimuth"] = flight.segment_azimuth()`` if they
111
+ want to use wind shear effects for a flight.
112
+ In the current implementation, any existing meteorological variables in the ``source``
113
+ are ignored. The ``source`` will be interpolated against the :attr:`met` dataset.
113
114
  params : Any
114
115
  Overwrite model parameters defined in ``__init__``.
115
116
 
@@ -122,7 +123,7 @@ class DryAdvection(models.Model):
122
123
  self.set_source(source)
123
124
  self.source = self.require_source_type(GeoVectorDataset)
124
125
 
125
- self._prepare_source()
126
+ self.source = self._prepare_source()
126
127
 
127
128
  interp_kwargs = self.interp_kwargs
128
129
 
@@ -142,7 +143,7 @@ class DryAdvection(models.Model):
142
143
  evolved = []
143
144
  for t in timesteps:
144
145
  filt = (source_time < t) & (source_time >= t - dt_integration)
145
- vector = self.source.filter(filt) + vector
146
+ vector = self.source.filter(filt, copy=False) + vector
146
147
  vector = _evolve_one_step(
147
148
  self.met,
148
149
  vector,
@@ -162,49 +163,44 @@ class DryAdvection(models.Model):
162
163
 
163
164
  return GeoVectorDataset.sum(evolved, fill_value=np.nan)
164
165
 
165
- def _prepare_source(self) -> None:
166
+ def _prepare_source(self) -> GeoVectorDataset:
166
167
  r"""Prepare :attr:`source` vector for advection by wind-shear-derived variables.
167
168
 
168
- This method adds the following variables to :attr:`source` if the `"azimuth"`
169
- parameter is not None:
169
+ The following variables are always guaranteed to be present in :attr:`source`:
170
170
 
171
171
  - ``age``: Age of plume.
172
+ - ``waypoint``: Identifier for each waypoint.
173
+
174
+ If `"azimuth"` is present in :attr:`source`, `source.attrs`, or :attr:`params`,
175
+ the following variables will also be added:
176
+
172
177
  - ``azimuth``: Initial plume direction, measured in clockwise direction from
173
- true north, [:math:`\deg`].
178
+ true north, [:math:`\deg`].
174
179
  - ``width``: Initial plume width, [:math:`m`].
175
180
  - ``depth``: Initial plume depth, [:math:`m`].
176
181
  - ``sigma_yz``: All zeros for cross-term term in covariance matrix of plume.
177
- """
178
182
 
183
+ Returns
184
+ -------
185
+ GeoVectorDataset
186
+ A filtered version of the source with only the required columns.
187
+ """
179
188
  self.source.setdefault("level", self.source.level)
180
-
181
- columns: tuple[str, ...] = ("longitude", "latitude", "level", "time")
182
- if "azimuth" in self.source:
183
- columns += ("azimuth",)
184
- self.source = GeoVectorDataset(self.source.select(columns, copy=False))
185
-
186
- # Get waypoint index if not already set
189
+ self.source["age"] = np.full(self.source.size, np.timedelta64(0, "ns"))
187
190
  self.source.setdefault("waypoint", np.arange(self.source.size))
188
191
 
189
- self.source["age"] = np.full(self.source.size, np.timedelta64(0, "ns"))
192
+ columns = ["longitude", "latitude", "level", "time", "age", "waypoint"]
193
+ azimuth = self.get_source_param("azimuth", set_attr=False)
194
+ if azimuth is None:
195
+ # Early exit for pointwise only simulation
196
+ if self.params["width"] is not None or self.params["depth"] is not None:
197
+ raise ValueError(
198
+ "If 'azimuth' is None, then 'width' and 'depth' must also be None."
199
+ )
200
+ return GeoVectorDataset(self.source.select(columns, copy=False), copy=False)
190
201
 
191
202
  if "azimuth" not in self.source:
192
- if isinstance(self.source, Flight):
193
- pointwise_only = False
194
- self.source["azimuth"] = self.source.segment_azimuth()
195
- else:
196
- try:
197
- self.source.broadcast_attrs("azimuth")
198
- except KeyError:
199
- if (azimuth := self.params["azimuth"]) is not None:
200
- pointwise_only = False
201
- self.source["azimuth"] = np.full_like(self.source["longitude"], azimuth)
202
- else:
203
- pointwise_only = True
204
- else:
205
- pointwise_only = False
206
- else:
207
- pointwise_only = False
203
+ self.source["azimuth"] = np.full_like(self.source["longitude"], azimuth)
208
204
 
209
205
  for key in ("width", "depth"):
210
206
  if key in self.source:
@@ -214,18 +210,12 @@ class DryAdvection(models.Model):
214
210
  continue
215
211
 
216
212
  val = self.params[key]
217
- if val is None and not pointwise_only:
213
+ if val is None:
218
214
  raise ValueError(f"If '{key}' is None, then 'azimuth' must also be None.")
219
215
 
220
- if val is not None and pointwise_only:
221
- raise ValueError(f"Cannot specify '{key}' without specifying 'azimuth'.")
222
-
223
- if not pointwise_only:
224
- self.source[key] = np.full_like(self.source["longitude"], val)
225
-
226
- if pointwise_only:
227
- return
216
+ self.source[key] = np.full_like(self.source["longitude"], val)
228
217
 
218
+ columns.extend(["azimuth", "width", "depth", "sigma_yz", "area_eff"])
229
219
  self.source["sigma_yz"] = np.zeros_like(self.source["longitude"])
230
220
  width = self.source["width"]
231
221
  depth = self.source["depth"]
@@ -233,6 +223,8 @@ class DryAdvection(models.Model):
233
223
  width, depth, sigma_yz=0.0
234
224
  )
235
225
 
226
+ return GeoVectorDataset(self.source.select(columns, copy=False), copy=False)
227
+
236
228
 
237
229
  def _perform_interp_for_step(
238
230
  met: MetDataset,
@@ -412,15 +404,20 @@ def _calc_geometry(
412
404
  u_wind_tail = vector.data.pop("eastward_wind_tail")
413
405
  v_wind_tail = vector.data.pop("northward_wind_tail")
414
406
 
415
- longitude_head_t2 = geo.advect_longitude(
416
- longitude=longitude_head, latitude=latitude_head, u_wind=u_wind_head, dt=dt
407
+ longitude_head_t2, latitude_head_t2 = geo.advect_horizontal(
408
+ longitude=longitude_head,
409
+ latitude=latitude_head,
410
+ u_wind=u_wind_head,
411
+ v_wind=v_wind_head,
412
+ dt=dt,
417
413
  )
418
- latitude_head_t2 = geo.advect_latitude(latitude=latitude_head, v_wind=v_wind_head, dt=dt)
419
-
420
- longitude_tail_t2 = geo.advect_longitude(
421
- longitude=longitude_tail, latitude=latitude_tail, u_wind=u_wind_tail, dt=dt
414
+ longitude_tail_t2, latitude_tail_t2 = geo.advect_horizontal(
415
+ longitude=longitude_tail,
416
+ latitude=latitude_tail,
417
+ u_wind=u_wind_tail,
418
+ v_wind=v_wind_tail,
419
+ dt=dt,
422
420
  )
423
- latitude_tail_t2 = geo.advect_latitude(latitude=latitude_tail, v_wind=v_wind_tail, dt=dt)
424
421
 
425
422
  azimuth_2 = geo.azimuth(
426
423
  lons0=longitude_tail_t2,
@@ -453,8 +450,7 @@ def _evolve_one_step(
453
450
  longitude = vector["longitude"]
454
451
 
455
452
  dt = t - vector["time"]
456
- longitude_2 = geo.advect_longitude(longitude, latitude, u_wind, dt) # type: ignore[arg-type]
457
- latitude_2 = geo.advect_latitude(latitude, v_wind, dt) # type: ignore[arg-type]
453
+ longitude_2, latitude_2 = geo.advect_horizontal(longitude, latitude, u_wind, v_wind, dt) # type: ignore[arg-type]
458
454
  level_2 = geo.advect_level(
459
455
  vector.level,
460
456
  vertical_velocity,
@@ -7,14 +7,19 @@ import contextlib
7
7
  import dataclasses
8
8
  import functools
9
9
  import pathlib
10
+ import sys
10
11
  import warnings
11
12
  from typing import Any, NoReturn, overload
12
13
 
14
+ if sys.version_info >= (3, 12):
15
+ from typing import override
16
+ else:
17
+ from typing_extensions import override
18
+
13
19
  import numpy as np
14
20
  import numpy.typing as npt
15
21
  import pandas as pd
16
22
  import xarray as xr
17
- from overrides import overrides
18
23
 
19
24
  from pycontrails.core import models
20
25
  from pycontrails.core.met import MetDataArray, MetDataset
@@ -202,7 +207,7 @@ class ConstantHumidityScaling(HumidityScaling):
202
207
  default_params = ConstantHumidityScalingParams
203
208
  scaler_specific_keys = ("rhi_adj",)
204
209
 
205
- @overrides
210
+ @override
206
211
  def scale(
207
212
  self,
208
213
  specific_humidity: ArrayLike,
@@ -254,7 +259,7 @@ class ExponentialBoostHumidityScaling(HumidityScaling):
254
259
  default_params = ExponentialBoostHumidityScalingParams
255
260
  scaler_specific_keys = "rhi_adj", "rhi_boost_exponent", "clip_upper"
256
261
 
257
- @overrides
262
+ @override
258
263
  def scale(
259
264
  self,
260
265
  specific_humidity: ArrayLike,
@@ -408,7 +413,7 @@ class ExponentialBoostLatitudeCorrectionHumidityScaling(HumidityScaling):
408
413
  q_method = self.params["interpolation_q_method"]
409
414
  return {**super()._scale_kwargs(), "q_method": q_method}
410
415
 
411
- @overrides
416
+ @override
412
417
  def scale(
413
418
  self,
414
419
  specific_humidity: ArrayLike,
@@ -557,7 +562,7 @@ class HumidityScalingByLevel(HumidityScaling):
557
562
  "stratosphere_threshold",
558
563
  )
559
564
 
560
- @overrides
565
+ @override
561
566
  def scale(
562
567
  self,
563
568
  specific_humidity: ArrayLike,
@@ -825,7 +830,7 @@ class HistogramMatching(HumidityScaling):
825
830
  warnings.warn(msg, DeprecationWarning)
826
831
  super().__init__(met, params, **params_kwargs)
827
832
 
828
- @overrides
833
+ @override
829
834
  def scale(
830
835
  self,
831
836
  specific_humidity: ArrayLike,
@@ -976,7 +981,7 @@ class HistogramMatchingWithEckel(HumidityScaling):
976
981
 
977
982
  return self.source
978
983
 
979
- @overrides
984
+ @override
980
985
  def scale( # type: ignore[override]
981
986
  self,
982
987
  specific_humidity: npt.NDArray[np.float64],
@@ -4,7 +4,7 @@ from pycontrails.models.ps_model.ps_aircraft_params import (
4
4
  PSAircraftEngineParams,
5
5
  load_aircraft_engine_params,
6
6
  )
7
- from pycontrails.models.ps_model.ps_grid import PSGrid, ps_nominal_grid
7
+ from pycontrails.models.ps_model.ps_grid import PSGrid, ps_nominal_grid, ps_nominal_optimize_mach
8
8
  from pycontrails.models.ps_model.ps_model import PSFlight, PSFlightParams
9
9
 
10
10
  __all__ = [
@@ -14,4 +14,5 @@ __all__ = [
14
14
  "PSGrid",
15
15
  "load_aircraft_engine_params",
16
16
  "ps_nominal_grid",
17
+ "ps_nominal_optimize_mach",
17
18
  ]
@@ -11,6 +11,7 @@ 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
15
16
 
16
17
  #: Path to the Poll-Schumann aircraft parameters CSV file.
@@ -193,7 +194,7 @@ def _row_to_aircraft_engine_params(tup: Any) -> tuple[str, PSAircraftEngineParam
193
194
 
194
195
  @functools.cache
195
196
  def load_aircraft_engine_params(
196
- engine_deterioration_factor: float = 0.025,
197
+ engine_deterioration_factor: float = AircraftPerformanceParams.engine_deterioration_factor,
197
198
  ) -> Mapping[str, PSAircraftEngineParams]:
198
199
  """
199
200
  Extract aircraft-engine parameters for each aircraft type supported by the PS model.
@@ -254,7 +255,7 @@ def load_aircraft_engine_params(
254
255
  }
255
256
 
256
257
  df = pd.read_csv(PS_FILE_PATH, dtype=dtypes)
257
- df["eta_1"] = df["eta_1"] * (1.0 - engine_deterioration_factor)
258
+ df["eta_1"] *= 1.0 - engine_deterioration_factor
258
259
 
259
260
  return dict(_row_to_aircraft_engine_params(tup) for tup in df.itertuples(index=False))
260
261
 
@@ -333,6 +333,7 @@ def ps_nominal_grid(
333
333
  q_fuel: float = JetA.q_fuel,
334
334
  mach_number: float | None = None,
335
335
  maxiter: int = PSGridParams.maxiter,
336
+ engine_deterioration_factor: float = PSGridParams.engine_deterioration_factor,
336
337
  ) -> xr.Dataset:
337
338
  """Calculate the nominal performance grid for a given aircraft type.
338
339
 
@@ -359,6 +360,9 @@ def ps_nominal_grid(
359
360
  The Mach number. If None (default), the PS design Mach number is used.
360
361
  maxiter : int, optional
361
362
  Passed into :func:`scipy.optimize.newton`.
363
+ engine_deterioration_factor : float, optional
364
+ The engine deterioration factor,
365
+ by default :attr:`PSGridParams.engine_deterioration_factor`.
362
366
 
363
367
  Returns
364
368
  -------
@@ -428,7 +432,7 @@ def ps_nominal_grid(
428
432
 
429
433
  air_pressure = level * 100.0
430
434
 
431
- aircraft_engine_params = ps_model.load_aircraft_engine_params()
435
+ aircraft_engine_params = ps_model.load_aircraft_engine_params(engine_deterioration_factor)
432
436
 
433
437
  try:
434
438
  atyp_param = aircraft_engine_params[aircraft_type]
@@ -503,3 +507,185 @@ def ps_nominal_grid(
503
507
  coords=coords,
504
508
  attrs=attrs,
505
509
  )
510
+
511
+
512
+ def _newton_mach(
513
+ mach_number: ArrayOrFloat,
514
+ perf: _PerfVariables,
515
+ aircraft_mass: ArrayOrFloat,
516
+ headwind: ArrayOrFloat,
517
+ cost_index: ArrayOrFloat,
518
+ ) -> ArrayOrFloat:
519
+ """Approximate the derivative of the cost of a segment based on mach number.
520
+
521
+ This is used to find the mach number at which cost in minimized.
522
+ """
523
+ perf.mach_number = mach_number + 1e-4
524
+ tas = units.mach_number_to_tas(perf.mach_number, perf.air_temperature)
525
+ groundspeed = tas - headwind
526
+ ff1 = _nominal_perf(aircraft_mass, perf).fuel_flow
527
+ eccf1 = (cost_index + ff1 * 60) / groundspeed
528
+
529
+ perf.mach_number = mach_number - 1e-4
530
+ tas = units.mach_number_to_tas(perf.mach_number, perf.air_temperature)
531
+ groundspeed = tas - headwind
532
+ ff2 = _nominal_perf(aircraft_mass, perf).fuel_flow
533
+ eccf2 = (cost_index + ff2 * 60) / groundspeed
534
+ return eccf1 - eccf2
535
+
536
+
537
+ def ps_nominal_optimize_mach(
538
+ aircraft_type: str,
539
+ aircraft_mass: ArrayOrFloat,
540
+ cost_index: ArrayOrFloat,
541
+ level: ArrayOrFloat,
542
+ *,
543
+ air_temperature: ArrayOrFloat | None = None,
544
+ northward_wind: ArrayOrFloat | None = None,
545
+ eastward_wind: ArrayOrFloat | None = None,
546
+ sin_a: ArrayOrFloat | None = None,
547
+ cos_a: ArrayOrFloat | None = None,
548
+ q_fuel: float = JetA.q_fuel,
549
+ engine_deterioration_factor: float = PSGridParams.engine_deterioration_factor,
550
+ ) -> xr.Dataset:
551
+ """Calculate the nominal optimal mach number for a given aircraft type.
552
+
553
+ This function is similar to the :class:`ps_nominal_grid` method, but rather than
554
+ maximizing engine efficiecy by adjusting aircraft, we are minimizing cost by adjusting
555
+ mach number.
556
+
557
+ Parameters
558
+ ----------
559
+ aircraft_type : str
560
+ The aircraft type.
561
+ aircraft_mass: ArrayOrFloat
562
+ The aircraft mass, [:math:`kg`].
563
+ cost_index: ArrayOrFloat
564
+ The cost index, [:math:`kg/min`], or non-fuel cost of one minute of flight time
565
+ level : ArrayOrFloat
566
+ The pressure level, [:math:`hPa`]. If a :class:`numpy.ndarray` is passed, it is
567
+ assumed to be one dimensional and the same length as the``aircraft_mass`` argument.
568
+ air_temperature : ArrayOrFloat | None, optional
569
+ The ambient air temperature, [:math:`K`]. If None (default), the ISA
570
+ temperature is computed from the ``level`` argument. If a :class:`numpy.ndarray`
571
+ is passed, it is assumed to be one dimensional and the same length as the
572
+ ``aircraft_mass`` argument.
573
+ air_temperature : ArrayOrFloat | None, optional
574
+ northward_wind: ArrayOrFloat | None = None, optional
575
+ The northward component of winds, [:math:`m/s`]. If None (default) assumed to be
576
+ zero.
577
+ eastward_wind: ArrayOrFloat | None = None, optional
578
+ The eastward component of winds, [:math:`m/s`]. If None (default) assumed to be
579
+ zero.
580
+ sin_a: ArrayOrFloat | None = None, optional
581
+ The sine between the true bearing of flight and the longitudinal axis. Must be
582
+ specified if wind data is provided. Will be ignored if wind data is not provided.
583
+ cos_a: ArrayOrFloat | None = None, optional
584
+ The cosine between the true bearing of flight and the longitudinal axis. Must be
585
+ specified if wind data is provided. Will be ignored if wind data is not provided.
586
+ q_fuel : float, optional
587
+ The fuel heating value, by default :attr:`JetA.q_fuel`.
588
+ engine_deterioration_factor : float, optional
589
+ The engine deterioration factor,
590
+ by default :attr:`PSGridParams.engine_deterioration_factor`.
591
+
592
+ Returns
593
+ -------
594
+ xr.Dataset
595
+ The nominal performance grid. The grid is indexed by altitude.
596
+ Contains the following variables:
597
+
598
+ - ``"mach_number"``: The mach number that minimizes segment cost
599
+ - ``"fuel_flow"`` : Fuel flow rate, [:math:`kg/s`]
600
+ - ``"engine_efficiency"`` : Engine efficiency
601
+ - ``"aircraft_mass"`` : Aircraft mass,
602
+ [:math:`kg`]
603
+
604
+ Raises
605
+ ------
606
+ KeyError
607
+ If "aircraft_type" is not supported by the PS model.
608
+ ValueError
609
+ If wind data is provided without segment angles.
610
+ """
611
+ dims = ("level",)
612
+ coords = {"level": level}
613
+ aircraft_engine_params = ps_model.load_aircraft_engine_params(engine_deterioration_factor)
614
+ try:
615
+ atyp_param = aircraft_engine_params[aircraft_type]
616
+ except KeyError as exc:
617
+ msg = (
618
+ f"The aircraft type {aircraft_type} is not currently supported by the PS model. "
619
+ f"Available aircraft types are: {list(aircraft_engine_params)}"
620
+ )
621
+ raise KeyError(msg) from exc
622
+
623
+ if air_temperature is None:
624
+ altitude_m = units.pl_to_m(level)
625
+ air_temperature = units.m_to_T_isa(altitude_m)
626
+
627
+ headwind: ArrayOrFloat
628
+ if northward_wind is not None and eastward_wind is not None:
629
+ if sin_a is None or cos_a is None:
630
+ msg = "Segment angles must be provide if wind data is specified"
631
+ raise ValueError(msg)
632
+ headwind = -(northward_wind * cos_a + eastward_wind * sin_a)
633
+ else:
634
+ headwind = 0.0 # type: ignore
635
+
636
+ min_mach = ps_operational_limits.minimum_mach_num(
637
+ air_pressure=level * 100.0,
638
+ aircraft_mass=aircraft_mass,
639
+ atyp_param=atyp_param,
640
+ )
641
+
642
+ max_mach = ps_operational_limits.maximum_mach_num(
643
+ altitude_ft=units.pl_to_ft(level),
644
+ air_pressure=level * 100.0,
645
+ aircraft_mass=aircraft_mass,
646
+ air_temperature=air_temperature,
647
+ theta=np.full_like(aircraft_mass, 0.0),
648
+ atyp_param=atyp_param,
649
+ )
650
+
651
+ x0 = (min_mach + max_mach) / 2.0 # type: ignore
652
+
653
+ perf = _PerfVariables(
654
+ atyp_param=atyp_param,
655
+ air_pressure=level * 100.0,
656
+ air_temperature=air_temperature,
657
+ mach_number=x0,
658
+ q_fuel=q_fuel,
659
+ )
660
+
661
+ opt_mach = scipy.optimize.newton(
662
+ func=_newton_mach,
663
+ args=(perf, aircraft_mass, headwind, cost_index),
664
+ x0=x0,
665
+ tol=1e-4,
666
+ disp=False,
667
+ ).clip(min=min_mach, max=max_mach)
668
+
669
+ perf.mach_number = opt_mach
670
+ output = _nominal_perf(aircraft_mass, perf)
671
+
672
+ engine_efficiency = output.engine_efficiency
673
+ fuel_flow = output.fuel_flow
674
+
675
+ attrs = {
676
+ "aircraft_type": aircraft_type,
677
+ "q_fuel": q_fuel,
678
+ "wingspan": atyp_param.wing_span,
679
+ "n_engine": atyp_param.n_engine,
680
+ }
681
+
682
+ return xr.Dataset(
683
+ {
684
+ "mach_number": (dims, opt_mach),
685
+ "aircraft_mass": (dims, aircraft_mass),
686
+ "engine_efficiency": (dims, engine_efficiency),
687
+ "fuel_flow": (dims, fuel_flow),
688
+ },
689
+ coords=coords,
690
+ attrs=attrs,
691
+ )
@@ -5,13 +5,18 @@ from __future__ import annotations
5
5
  import dataclasses
6
6
  import functools
7
7
  import pathlib
8
+ import sys
8
9
  from collections.abc import Mapping
9
10
  from typing import Any, NoReturn, overload
10
11
 
12
+ if sys.version_info >= (3, 12):
13
+ from typing import override
14
+ else:
15
+ from typing_extensions import override
16
+
11
17
  import numpy as np
12
18
  import numpy.typing as npt
13
19
  import pandas as pd
14
- from overrides import overrides
15
20
 
16
21
  from pycontrails.core import flight
17
22
  from pycontrails.core.aircraft_performance import (
@@ -46,13 +51,6 @@ class PSFlightParams(AircraftPerformanceParams):
46
51
  #: efficiency to always exceed this value.
47
52
  eta_over_eta_b_min: float | None = 0.5
48
53
 
49
- #: Account for "in-service" engine deterioration between maintenance cycles.
50
- #: Default value is set to +2.5% increase in fuel consumption.
51
- # Reference:
52
- # Gurrola Arrieta, M.D.J., Botez, R.M. and Lasne, A., 2024. An Engine Deterioration Model for
53
- # Predicting Fuel Consumption Impact in a Regional Aircraft. Aerospace, 11(6), p.426.
54
- engine_deterioration_factor: float = 0.025
55
-
56
54
 
57
55
  class PSFlight(AircraftPerformance):
58
56
  """Simulate aircraft performance using Poll-Schumann (PS) model.
@@ -65,6 +63,10 @@ class PSFlight(AircraftPerformance):
65
63
  Poll & Schumann (2022). An estimation method for the fuel burn and other performance
66
64
  characteristics of civil transport aircraft. Part 3 Generalisation to cover climb,
67
65
  descent and holding. Aero. J., submitted.
66
+
67
+ See Also
68
+ --------
69
+ pycontrails.physics.jet.aircraft_load_factor
68
70
  """
69
71
 
70
72
  name = "PSFlight"
@@ -125,7 +127,7 @@ class PSFlight(AircraftPerformance):
125
127
  @overload
126
128
  def eval(self, source: None = ..., **params: Any) -> NoReturn: ...
127
129
 
128
- @overrides
130
+ @override
129
131
  def eval(self, source: Flight | None = None, **params: Any) -> Flight:
130
132
  self.update_params(params)
131
133
  self.set_source(source)
@@ -217,7 +219,7 @@ class PSFlight(AircraftPerformance):
217
219
 
218
220
  return fl
219
221
 
220
- @overrides
222
+ @override
221
223
  def calculate_aircraft_performance(
222
224
  self,
223
225
  *,