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.
- pycontrails/_version.py +2 -2
- pycontrails/core/aircraft_performance.py +24 -5
- pycontrails/core/cache.py +14 -10
- pycontrails/core/fleet.py +22 -12
- pycontrails/core/flight.py +25 -15
- pycontrails/core/met.py +34 -22
- pycontrails/core/rgi_cython.cp313-win_amd64.pyd +0 -0
- pycontrails/core/vector.py +38 -38
- pycontrails/datalib/ecmwf/arco_era5.py +10 -5
- pycontrails/datalib/ecmwf/common.py +7 -2
- pycontrails/datalib/ecmwf/era5.py +9 -4
- pycontrails/datalib/ecmwf/era5_model_level.py +9 -5
- pycontrails/datalib/ecmwf/hres.py +12 -7
- pycontrails/datalib/ecmwf/hres_model_level.py +10 -5
- pycontrails/datalib/ecmwf/ifs.py +11 -6
- pycontrails/datalib/ecmwf/variables.py +1 -0
- pycontrails/datalib/gfs/gfs.py +52 -34
- pycontrails/datalib/gfs/variables.py +6 -2
- pycontrails/datalib/landsat.py +5 -8
- pycontrails/datalib/sentinel.py +7 -11
- pycontrails/ext/bada.py +3 -2
- pycontrails/ext/synthetic_flight.py +3 -2
- pycontrails/models/accf.py +40 -19
- pycontrails/models/apcemm/apcemm.py +2 -1
- pycontrails/models/cocip/cocip.py +8 -4
- pycontrails/models/cocipgrid/cocip_grid.py +25 -20
- pycontrails/models/dry_advection.py +50 -54
- pycontrails/models/humidity_scaling/humidity_scaling.py +12 -7
- pycontrails/models/ps_model/__init__.py +2 -1
- pycontrails/models/ps_model/ps_aircraft_params.py +3 -2
- pycontrails/models/ps_model/ps_grid.py +187 -1
- pycontrails/models/ps_model/ps_model.py +12 -10
- pycontrails/models/ps_model/ps_operational_limits.py +39 -52
- pycontrails/physics/geo.py +149 -0
- pycontrails/physics/jet.py +141 -11
- pycontrails/physics/static/iata-cargo-load-factors-20241115.csv +71 -0
- pycontrails/physics/static/iata-passenger-load-factors-20241115.csv +71 -0
- {pycontrails-0.54.1.dist-info → pycontrails-0.54.3.dist-info}/METADATA +12 -11
- {pycontrails-0.54.1.dist-info → pycontrails-0.54.3.dist-info}/RECORD +43 -41
- {pycontrails-0.54.1.dist-info → pycontrails-0.54.3.dist-info}/WHEEL +1 -1
- {pycontrails-0.54.1.dist-info → pycontrails-0.54.3.dist-info}/LICENSE +0 -0
- {pycontrails-0.54.1.dist-info → pycontrails-0.54.3.dist-info}/NOTICE +0 -0
- {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) ->
|
|
166
|
+
def _prepare_source(self) -> GeoVectorDataset:
|
|
166
167
|
r"""Prepare :attr:`source` vector for advection by wind-shear-derived variables.
|
|
167
168
|
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
213
|
+
if val is None:
|
|
218
214
|
raise ValueError(f"If '{key}' is None, then 'azimuth' must also be None.")
|
|
219
215
|
|
|
220
|
-
|
|
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.
|
|
416
|
-
longitude=longitude_head,
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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.
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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 =
|
|
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"]
|
|
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
|
-
@
|
|
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
|
-
@
|
|
222
|
+
@override
|
|
221
223
|
def calculate_aircraft_performance(
|
|
222
224
|
self,
|
|
223
225
|
*,
|