pycontrails 0.54.2__cp310-cp310-macosx_11_0_arm64.whl → 0.54.3__cp310-cp310-macosx_11_0_arm64.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 +17 -3
- pycontrails/core/flight.py +3 -1
- pycontrails/core/rgi_cython.cpython-310-darwin.so +0 -0
- pycontrails/datalib/ecmwf/variables.py +1 -0
- 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 +1 -2
- pycontrails/models/cocipgrid/cocip_grid.py +25 -20
- pycontrails/models/dry_advection.py +50 -54
- 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 +4 -7
- 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.2.dist-info → pycontrails-0.54.3.dist-info}/METADATA +9 -9
- {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/RECORD +29 -27
- {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/WHEEL +1 -1
- {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/LICENSE +0 -0
- {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/NOTICE +0 -0
- {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/top_level.txt +0 -0
pycontrails/_version.py
CHANGED
|
@@ -26,7 +26,9 @@ from pycontrails.physics import jet
|
|
|
26
26
|
from pycontrails.utils.types import ArrayOrFloat
|
|
27
27
|
|
|
28
28
|
#: Default load factor for aircraft performance models.
|
|
29
|
-
|
|
29
|
+
#: See :func:`pycontrails.physics.jet.aircraft_load_factor`
|
|
30
|
+
#: for a higher precision approach to estimating the load factor.
|
|
31
|
+
DEFAULT_LOAD_FACTOR = 0.83
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
# --------------------------------------
|
|
@@ -35,7 +37,19 @@ DEFAULT_LOAD_FACTOR = 0.7
|
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
@dataclasses.dataclass
|
|
38
|
-
class
|
|
40
|
+
class CommonAircraftPerformanceParams:
|
|
41
|
+
"""Params for :class:`AircraftPerformanceParams` and :class:`AircraftPerformanceGridParams`."""
|
|
42
|
+
|
|
43
|
+
#: Account for "in-service" engine deterioration between maintenance cycles.
|
|
44
|
+
#: Default value is set to +2.5% increase in fuel consumption.
|
|
45
|
+
#: Reference:
|
|
46
|
+
#: Gurrola Arrieta, M.D.J., Botez, R.M. and Lasne, A., 2024. An Engine Deterioration Model for
|
|
47
|
+
#: Predicting Fuel Consumption Impact in a Regional Aircraft. Aerospace, 11(6), p.426.
|
|
48
|
+
engine_deterioration_factor: float = 0.025
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclasses.dataclass
|
|
52
|
+
class AircraftPerformanceParams(ModelParams, CommonAircraftPerformanceParams):
|
|
39
53
|
"""Parameters for :class:`AircraftPerformance`."""
|
|
40
54
|
|
|
41
55
|
#: Whether to correct fuel flow to ensure it remains within
|
|
@@ -547,7 +561,7 @@ class AircraftPerformanceData:
|
|
|
547
561
|
|
|
548
562
|
|
|
549
563
|
@dataclasses.dataclass
|
|
550
|
-
class AircraftPerformanceGridParams(ModelParams):
|
|
564
|
+
class AircraftPerformanceGridParams(ModelParams, CommonAircraftPerformanceParams):
|
|
551
565
|
"""Parameters for :class:`AircraftPerformanceGrid`."""
|
|
552
566
|
|
|
553
567
|
#: Fuel type
|
pycontrails/core/flight.py
CHANGED
|
@@ -121,6 +121,8 @@ class Flight(GeoVectorDataset):
|
|
|
121
121
|
calculations with the ICAO Aircraft Emissions Databank (EDB).
|
|
122
122
|
- ``max_mach_number``: Maximum Mach number at cruise altitude. Used by
|
|
123
123
|
some aircraft performance models to clip true airspeed.
|
|
124
|
+
- ``load_factor``: The load factor used in determining the aircraft's
|
|
125
|
+
take-off weight. Used by some aircraft performance models.
|
|
124
126
|
|
|
125
127
|
Numeric quantities that are constant over the entire flight trajectory
|
|
126
128
|
should be included as attributes.
|
|
@@ -961,7 +963,7 @@ class Flight(GeoVectorDataset):
|
|
|
961
963
|
msg = f"{msg} Pass 'keep_original_index=True' to keep the original index."
|
|
962
964
|
warnings.warn(msg)
|
|
963
965
|
|
|
964
|
-
return Flight(data=df, attrs=self.attrs)
|
|
966
|
+
return Flight(data=df, attrs=self.attrs, fuel=self.fuel)
|
|
965
967
|
|
|
966
968
|
def clean_and_resample(
|
|
967
969
|
self,
|
|
Binary file
|
|
@@ -107,6 +107,7 @@ RelativeHumidity = MetVariable(
|
|
|
107
107
|
long_name=met_var.RelativeHumidity.long_name,
|
|
108
108
|
units="%",
|
|
109
109
|
level_type=met_var.RelativeHumidity.level_type,
|
|
110
|
+
grib1_id=met_var.RelativeHumidity.grib1_id,
|
|
110
111
|
ecmwf_id=met_var.RelativeHumidity.ecmwf_id,
|
|
111
112
|
grib2_id=met_var.RelativeHumidity.grib2_id,
|
|
112
113
|
description=(
|
pycontrails/datalib/landsat.py
CHANGED
|
@@ -152,7 +152,7 @@ class Landsat:
|
|
|
152
152
|
are used. Bands must share a common resolution. The resolutions of each band are:
|
|
153
153
|
|
|
154
154
|
- B1-B7, B9: 30 m
|
|
155
|
-
-
|
|
155
|
+
- B8: 15 m
|
|
156
156
|
- B10, B11: 30 m (upsampled from true resolution of 100 m)
|
|
157
157
|
|
|
158
158
|
cachestore : cache.CacheStore, optional
|
|
@@ -291,9 +291,7 @@ def _check_band_resolution(bands: set[str]) -> None:
|
|
|
291
291
|
there are two valid cases: only band 8, or any bands except band 8.
|
|
292
292
|
"""
|
|
293
293
|
groups = [
|
|
294
|
-
{
|
|
295
|
-
"B8",
|
|
296
|
-
}, # 15 m
|
|
294
|
+
{"B8"}, # 15 m
|
|
297
295
|
{f"B{i}" for i in range(1, 12) if i != 8}, # 30 m
|
|
298
296
|
]
|
|
299
297
|
if not any(bands.issubset(group) for group in groups):
|
|
@@ -313,10 +311,9 @@ def _read(path: str, meta: str, band: str, processing: str) -> xr.DataArray:
|
|
|
313
311
|
pycontrails_optional_package="sat",
|
|
314
312
|
)
|
|
315
313
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
src.close()
|
|
314
|
+
with rasterio.open(path) as src:
|
|
315
|
+
img = src.read(1)
|
|
316
|
+
crs = pyproj.CRS.from_epsg(src.crs.to_epsg())
|
|
320
317
|
|
|
321
318
|
if processing == "reflectance":
|
|
322
319
|
mult, add = _read_band_reflectance_rescaling(meta, band)
|
pycontrails/datalib/sentinel.py
CHANGED
|
@@ -313,9 +313,8 @@ def _check_band_resolution(bands: set[str]) -> None:
|
|
|
313
313
|
def _read(path: str, granule_meta: str, safe_meta: str, band: str, processing: str) -> xr.DataArray:
|
|
314
314
|
"""Read imagery data from Sentinel-2 files."""
|
|
315
315
|
Image.MAX_IMAGE_PIXELS = None # avoid decompression bomb warning
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
src.close()
|
|
316
|
+
with Image.open(path) as src:
|
|
317
|
+
img = np.asarray(src)
|
|
319
318
|
|
|
320
319
|
if processing == "reflectance":
|
|
321
320
|
gain, offset = _read_band_reflectance_rescaling(safe_meta, band)
|
|
@@ -357,10 +356,9 @@ def _band_id(band: str) -> int:
|
|
|
357
356
|
"""Get band ID used in some metadata files."""
|
|
358
357
|
if band in (f"B{i:2d}" for i in range(1, 9)):
|
|
359
358
|
return int(band[1:]) - 1
|
|
360
|
-
|
|
359
|
+
if band == "B8A":
|
|
361
360
|
return 8
|
|
362
|
-
|
|
363
|
-
return int(band[1:])
|
|
361
|
+
return int(band[1:])
|
|
364
362
|
|
|
365
363
|
|
|
366
364
|
def _read_band_reflectance_rescaling(meta: str, band: str) -> tuple[float, float]:
|
|
@@ -389,12 +387,10 @@ def _read_band_reflectance_rescaling(meta: str, band: str) -> tuple[float, float
|
|
|
389
387
|
for elem in elems:
|
|
390
388
|
if int(elem.attrib["band_id"]) == band_id and elem.text is not None:
|
|
391
389
|
offset = float(elem.text)
|
|
392
|
-
|
|
393
|
-
else:
|
|
394
|
-
msg = f"Could not find reflectance offset for band {band} (band ID {band_id})"
|
|
395
|
-
raise ValueError(msg)
|
|
390
|
+
return gain, offset
|
|
396
391
|
|
|
397
|
-
|
|
392
|
+
msg = f"Could not find reflectance offset for band {band} (band ID {band_id})"
|
|
393
|
+
raise ValueError(msg)
|
|
398
394
|
|
|
399
395
|
|
|
400
396
|
def _read_image_coordinates(meta: str, band: str) -> tuple[np.ndarray, np.ndarray]:
|
pycontrails/ext/bada.py
CHANGED
|
@@ -22,8 +22,9 @@ try:
|
|
|
22
22
|
|
|
23
23
|
except ImportError as e:
|
|
24
24
|
raise ImportError(
|
|
25
|
-
"Failed to import the 'pycontrails-bada' package. Install with 'pip install"
|
|
26
|
-
|
|
25
|
+
"Failed to import the 'pycontrails-bada' package. Install with 'pip install "
|
|
26
|
+
"--index-url https://us-central1-python.pkg.dev/contrails-301217/pycontrails/simple "
|
|
27
|
+
"pycontrails-bada'."
|
|
27
28
|
) from e
|
|
28
29
|
else:
|
|
29
30
|
__all__ = [
|
|
@@ -20,8 +20,9 @@ try:
|
|
|
20
20
|
from pycontrails.ext.bada import bada_model
|
|
21
21
|
except ImportError as e:
|
|
22
22
|
raise ImportError(
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
"Failed to import the 'pycontrails-bada' package. Install with 'pip install "
|
|
24
|
+
"--index-url https://us-central1-python.pkg.dev/contrails-301217/pycontrails/simple "
|
|
25
|
+
"pycontrails-bada'."
|
|
25
26
|
) from e
|
|
26
27
|
|
|
27
28
|
logger = logging.getLogger(__name__)
|
pycontrails/models/accf.py
CHANGED
|
@@ -88,13 +88,16 @@ class ACCFParams(ModelParams):
|
|
|
88
88
|
h2o_scaling: float = 1.0
|
|
89
89
|
o3_scaling: float = 1.0
|
|
90
90
|
|
|
91
|
-
forecast_step: float =
|
|
91
|
+
forecast_step: float | None = None
|
|
92
92
|
|
|
93
93
|
sep_ri_rw: bool = False
|
|
94
94
|
|
|
95
95
|
climate_indicator: str = "ATR"
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
#: The horizontal resolution of the meteorological data in degrees.
|
|
98
|
+
#: If None, it will be inferred from the ``met`` dataset for :class:`MetDataset`
|
|
99
|
+
#: source, otherwise it will be set to 0.5.
|
|
100
|
+
horizontal_resolution: float | None = None
|
|
98
101
|
|
|
99
102
|
emission_scenario: str = "pulse"
|
|
100
103
|
|
|
@@ -116,6 +119,8 @@ class ACCFParams(ModelParams):
|
|
|
116
119
|
|
|
117
120
|
PMO: bool = False
|
|
118
121
|
|
|
122
|
+
unit_K_per_kg_fuel: bool = False
|
|
123
|
+
|
|
119
124
|
|
|
120
125
|
class ACCF(Model):
|
|
121
126
|
"""Compute Algorithmic Climate Change Functions (ACCF).
|
|
@@ -146,16 +151,13 @@ class ACCF(Model):
|
|
|
146
151
|
SpecificHumidity,
|
|
147
152
|
ecmwf.PotentialVorticity,
|
|
148
153
|
Geopotential,
|
|
149
|
-
RelativeHumidity,
|
|
154
|
+
(RelativeHumidity, ecmwf.RelativeHumidity),
|
|
150
155
|
NorthwardWind,
|
|
151
156
|
EastwardWind,
|
|
152
|
-
ecmwf.PotentialVorticity,
|
|
153
157
|
)
|
|
154
158
|
sur_variables = (ecmwf.SurfaceSolarDownwardRadiation, ecmwf.TopNetThermalRadiation)
|
|
155
159
|
default_params = ACCFParams
|
|
156
160
|
|
|
157
|
-
short_vars = frozenset(v.short_name for v in (*met_variables, *sur_variables))
|
|
158
|
-
|
|
159
161
|
# This variable won't get used since we are not writing the output
|
|
160
162
|
# anywhere, but the library will complain if it's not defined
|
|
161
163
|
path_lib = "./"
|
|
@@ -168,7 +170,13 @@ class ACCF(Model):
|
|
|
168
170
|
**params_kwargs: Any,
|
|
169
171
|
) -> None:
|
|
170
172
|
# Normalize ECMWF variables
|
|
171
|
-
|
|
173
|
+
variables = (v[0] if isinstance(v, tuple) else v for v in self.met_variables)
|
|
174
|
+
met = standardize_variables(met, variables)
|
|
175
|
+
|
|
176
|
+
# If relative humidity is in percentage, convert to a proportion
|
|
177
|
+
if met["relative_humidity"].attrs.get("units") == "%":
|
|
178
|
+
met.data["relative_humidity"] /= 100.0
|
|
179
|
+
met.data["relative_humidity"].attrs["units"] = "1"
|
|
172
180
|
|
|
173
181
|
# Ignore humidity scaling warning
|
|
174
182
|
with warnings.catch_warnings():
|
|
@@ -231,18 +239,21 @@ class ACCF(Model):
|
|
|
231
239
|
if hasattr(self, "surface"):
|
|
232
240
|
self.surface = self.source.downselect_met(self.surface)
|
|
233
241
|
|
|
234
|
-
if
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
hres = abs(longitude[1] - longitude[0])
|
|
239
|
-
self.params["horizontal_resolution"] = float(hres)
|
|
240
|
-
|
|
241
|
-
else:
|
|
242
|
+
if self.params["horizontal_resolution"] is None:
|
|
243
|
+
if isinstance(self.source, MetDataset):
|
|
244
|
+
# Overwrite horizontal resolution to match met
|
|
245
|
+
longitude = self.source.data["longitude"].values
|
|
242
246
|
latitude = self.source.data["latitude"].values
|
|
243
|
-
if
|
|
247
|
+
if longitude.size > 1:
|
|
248
|
+
hres = abs(longitude[1] - longitude[0])
|
|
249
|
+
self.params["horizontal_resolution"] = float(hres)
|
|
250
|
+
elif latitude.size > 1:
|
|
244
251
|
hres = abs(latitude[1] - latitude[0])
|
|
245
252
|
self.params["horizontal_resolution"] = float(hres)
|
|
253
|
+
else:
|
|
254
|
+
self.params["horizontal_resolution"] = 0.5
|
|
255
|
+
else:
|
|
256
|
+
self.params["horizontal_resolution"] = 0.5
|
|
246
257
|
|
|
247
258
|
p_settings = _get_accf_config(self.params)
|
|
248
259
|
|
|
@@ -267,10 +278,14 @@ class ACCF(Model):
|
|
|
267
278
|
aCCFs, _ = clim_imp.get_xarray()
|
|
268
279
|
|
|
269
280
|
# assign ACCF outputs to source
|
|
281
|
+
skip = {
|
|
282
|
+
v[0].short_name if isinstance(v, tuple) else v.short_name
|
|
283
|
+
for v in (*self.met_variables, *self.sur_variables)
|
|
284
|
+
}
|
|
270
285
|
maCCFs = MetDataset(aCCFs)
|
|
271
286
|
for key, arr in maCCFs.data.items():
|
|
272
287
|
# skip met variables
|
|
273
|
-
if key in
|
|
288
|
+
if key in skip:
|
|
274
289
|
continue
|
|
275
290
|
|
|
276
291
|
assert isinstance(key, str)
|
|
@@ -292,7 +307,12 @@ class ACCF(Model):
|
|
|
292
307
|
# It also needs variables to have the ECMWF short name
|
|
293
308
|
if isinstance(self.met, MetDataset):
|
|
294
309
|
ds_met = self.met.data.transpose("time", "level", "latitude", "longitude")
|
|
295
|
-
name_dict = {
|
|
310
|
+
name_dict = {
|
|
311
|
+
v[0].standard_name if isinstance(v, tuple) else v.standard_name: v[0].short_name
|
|
312
|
+
if isinstance(v, tuple)
|
|
313
|
+
else v.short_name
|
|
314
|
+
for v in self.met_variables
|
|
315
|
+
}
|
|
296
316
|
ds_met = ds_met.rename(name_dict)
|
|
297
317
|
else:
|
|
298
318
|
ds_met = None
|
|
@@ -340,7 +360,7 @@ def _get_accf_config(params: dict[str, Any]) -> dict[str, Any]:
|
|
|
340
360
|
"horizontal_resolution": params["horizontal_resolution"],
|
|
341
361
|
"forecast_step": params["forecast_step"],
|
|
342
362
|
"NOx_aCCF": True,
|
|
343
|
-
"
|
|
363
|
+
"NOx_EI&F_km": params["nox_ei"],
|
|
344
364
|
"output_format": "netCDF",
|
|
345
365
|
"mean": False,
|
|
346
366
|
"std": False,
|
|
@@ -361,6 +381,7 @@ def _get_accf_config(params: dict[str, Any]) -> dict[str, Any]:
|
|
|
361
381
|
"H2O": params["h2o_scaling"],
|
|
362
382
|
"O3": params["o3_scaling"],
|
|
363
383
|
},
|
|
384
|
+
"unit_K/kg(fuel)": params["unit_K_per_kg_fuel"],
|
|
364
385
|
"PCFA": params["pfca"],
|
|
365
386
|
"PCFA-ISSR": {
|
|
366
387
|
"rhi_threshold": params["issr_rhi_threshold"],
|
|
@@ -28,6 +28,7 @@ from pycontrails.core.met_var import (
|
|
|
28
28
|
SpecificHumidity,
|
|
29
29
|
VerticalVelocity,
|
|
30
30
|
)
|
|
31
|
+
from pycontrails.core.vector import GeoVectorDataset
|
|
31
32
|
from pycontrails.models.apcemm import utils
|
|
32
33
|
from pycontrails.models.apcemm.inputs import APCEMMInput
|
|
33
34
|
from pycontrails.models.dry_advection import DryAdvection
|
|
@@ -314,7 +315,7 @@ class APCEMM(models.Model):
|
|
|
314
315
|
source: Flight
|
|
315
316
|
|
|
316
317
|
#: Output from trajectory calculation
|
|
317
|
-
trajectories:
|
|
318
|
+
trajectories: GeoVectorDataset | None
|
|
318
319
|
|
|
319
320
|
#: Time series output from the APCEMM early plume model
|
|
320
321
|
vortex: pd.DataFrame | None
|
|
@@ -2296,8 +2296,7 @@ def calc_timestep_contrail_evolution(
|
|
|
2296
2296
|
dt = time_2_array - time_1
|
|
2297
2297
|
|
|
2298
2298
|
# get new contrail location & segment properties after t_step
|
|
2299
|
-
longitude_2 = geo.
|
|
2300
|
-
latitude_2 = geo.advect_latitude(latitude_1, v_wind_1, dt)
|
|
2299
|
+
longitude_2, latitude_2 = geo.advect_horizontal(longitude_1, latitude_1, u_wind_1, v_wind_1, dt)
|
|
2301
2300
|
level_2 = geo.advect_level(level_1, vertical_velocity_1, rho_air_1, terminal_fall_speed_1, dt)
|
|
2302
2301
|
altitude_2 = units.pl_to_m(level_2)
|
|
2303
2302
|
|
|
@@ -816,6 +816,9 @@ class CocipGrid(models.Model):
|
|
|
816
816
|
"""
|
|
817
817
|
Shortcut to create a :class:`MetDataset` source from coordinate arrays.
|
|
818
818
|
|
|
819
|
+
.. versionchanged:: 0.54.3
|
|
820
|
+
By default, the returned latitude values now extend to the poles.
|
|
821
|
+
|
|
819
822
|
Parameters
|
|
820
823
|
----------
|
|
821
824
|
level : level: npt.NDArray[np.float64] | list[float] | float
|
|
@@ -829,8 +832,6 @@ class CocipGrid(models.Model):
|
|
|
829
832
|
longitude, latitude : npt.NDArray[np.float64] | list[float], optional
|
|
830
833
|
Longitude and latitude arrays, by default None. If not specified, values of
|
|
831
834
|
``lon_step`` and ``lat_step`` are used to define ``longitude`` and ``latitude``.
|
|
832
|
-
To avoid model degradation at the poles, latitude values are expected to be
|
|
833
|
-
between -80 and 80 degrees.
|
|
834
835
|
lon_step, lat_step : float, optional
|
|
835
836
|
Longitude and latitude resolution, by default 1.0.
|
|
836
837
|
Only used if parameter ``longitude`` (respective ``latitude``) not specified.
|
|
@@ -847,15 +848,11 @@ class CocipGrid(models.Model):
|
|
|
847
848
|
if longitude is None:
|
|
848
849
|
longitude = np.arange(-180, 180, lon_step, dtype=float)
|
|
849
850
|
if latitude is None:
|
|
850
|
-
latitude = np.arange(-
|
|
851
|
-
|
|
852
|
-
out = MetDataset.from_coords(longitude=longitude, latitude=latitude, level=level, time=time)
|
|
853
|
-
|
|
854
|
-
if np.any(out.data.latitude > 80.0001) or np.any(out.data.latitude < -80.0001):
|
|
855
|
-
msg = "Model only supports latitude between -80 and 80."
|
|
856
|
-
raise ValueError(msg)
|
|
851
|
+
latitude = np.arange(-90, 90.000001, lat_step, dtype=float)
|
|
857
852
|
|
|
858
|
-
return
|
|
853
|
+
return MetDataset.from_coords(
|
|
854
|
+
longitude=longitude, latitude=latitude, level=level, time=time
|
|
855
|
+
)
|
|
859
856
|
|
|
860
857
|
|
|
861
858
|
################################
|
|
@@ -2054,10 +2051,13 @@ def advect(
|
|
|
2054
2051
|
time_t2 = time + dt
|
|
2055
2052
|
age_t2 = age + dt
|
|
2056
2053
|
|
|
2057
|
-
longitude_t2 = geo.
|
|
2058
|
-
longitude=longitude,
|
|
2054
|
+
longitude_t2, latitude_t2 = geo.advect_horizontal(
|
|
2055
|
+
longitude=longitude,
|
|
2056
|
+
latitude=latitude,
|
|
2057
|
+
u_wind=u_wind,
|
|
2058
|
+
v_wind=v_wind,
|
|
2059
|
+
dt=dt,
|
|
2059
2060
|
)
|
|
2060
|
-
latitude_t2 = geo.advect_latitude(latitude=latitude, v_wind=v_wind, dt=dt)
|
|
2061
2061
|
level_t2 = geo.advect_level(level, vertical_velocity, rho_air, terminal_fall_speed, dt)
|
|
2062
2062
|
altitude_t2 = units.pl_to_m(level_t2)
|
|
2063
2063
|
|
|
@@ -2089,15 +2089,20 @@ def advect(
|
|
|
2089
2089
|
u_wind_tail = contrail["eastward_wind_tail"]
|
|
2090
2090
|
v_wind_tail = contrail["northward_wind_tail"]
|
|
2091
2091
|
|
|
2092
|
-
longitude_head_t2 = geo.
|
|
2093
|
-
longitude=longitude_head,
|
|
2092
|
+
longitude_head_t2, latitude_head_t2 = geo.advect_horizontal(
|
|
2093
|
+
longitude=longitude_head,
|
|
2094
|
+
latitude=latitude_head,
|
|
2095
|
+
u_wind=u_wind_head,
|
|
2096
|
+
v_wind=v_wind_head,
|
|
2097
|
+
dt=dt_head,
|
|
2094
2098
|
)
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
+
longitude_tail_t2, latitude_tail_t2 = geo.advect_horizontal(
|
|
2100
|
+
longitude=longitude_tail,
|
|
2101
|
+
latitude=latitude_tail,
|
|
2102
|
+
u_wind=u_wind_tail,
|
|
2103
|
+
v_wind=v_wind_tail,
|
|
2104
|
+
dt=dt_tail,
|
|
2099
2105
|
)
|
|
2100
|
-
latitude_tail_t2 = geo.advect_latitude(latitude=latitude_tail, v_wind=v_wind_tail, dt=dt_tail)
|
|
2101
2106
|
|
|
2102
2107
|
segment_length_t2 = geo.haversine(
|
|
2103
2108
|
lons0=longitude_head_t2,
|
|
@@ -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,
|
|
@@ -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
|
|