pycontrails 0.54.2__cp311-cp311-win_amd64.whl → 0.54.3__cp311-cp311-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 (29) hide show
  1. pycontrails/_version.py +2 -2
  2. pycontrails/core/aircraft_performance.py +17 -3
  3. pycontrails/core/flight.py +3 -1
  4. pycontrails/core/rgi_cython.cp311-win_amd64.pyd +0 -0
  5. pycontrails/datalib/ecmwf/variables.py +1 -0
  6. pycontrails/datalib/landsat.py +5 -8
  7. pycontrails/datalib/sentinel.py +7 -11
  8. pycontrails/ext/bada.py +3 -2
  9. pycontrails/ext/synthetic_flight.py +3 -2
  10. pycontrails/models/accf.py +40 -19
  11. pycontrails/models/apcemm/apcemm.py +2 -1
  12. pycontrails/models/cocip/cocip.py +1 -2
  13. pycontrails/models/cocipgrid/cocip_grid.py +25 -20
  14. pycontrails/models/dry_advection.py +50 -54
  15. pycontrails/models/ps_model/__init__.py +2 -1
  16. pycontrails/models/ps_model/ps_aircraft_params.py +3 -2
  17. pycontrails/models/ps_model/ps_grid.py +187 -1
  18. pycontrails/models/ps_model/ps_model.py +4 -7
  19. pycontrails/models/ps_model/ps_operational_limits.py +39 -52
  20. pycontrails/physics/geo.py +149 -0
  21. pycontrails/physics/jet.py +141 -11
  22. pycontrails/physics/static/iata-cargo-load-factors-20241115.csv +71 -0
  23. pycontrails/physics/static/iata-passenger-load-factors-20241115.csv +71 -0
  24. {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/METADATA +9 -9
  25. {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/RECORD +29 -27
  26. {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/WHEEL +1 -1
  27. {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/LICENSE +0 -0
  28. {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/NOTICE +0 -0
  29. {pycontrails-0.54.2.dist-info → pycontrails-0.54.3.dist-info}/top_level.txt +0 -0
pycontrails/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.54.2'
16
- __version_tuple__ = version_tuple = (0, 54, 2)
15
+ __version__ = version = '0.54.3'
16
+ __version_tuple__ = version_tuple = (0, 54, 3)
@@ -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
- DEFAULT_LOAD_FACTOR = 0.7
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 AircraftPerformanceParams(ModelParams):
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
@@ -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,
@@ -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=(
@@ -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
- - B9: 15 m
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
- src = rasterio.open(path)
317
- img = src.read(1)
318
- crs = pyproj.CRS.from_epsg(src.crs.to_epsg())
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)
@@ -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
- src = Image.open(path)
317
- img = np.asarray(src)
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
- elif band == "B8A":
359
+ if band == "B8A":
361
360
  return 8
362
- else:
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
- break
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
- return gain, offset
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
- ' "pycontrails-bada @ git+ssh://git@github.com/contrailcirrus/pycontrails-bada.git"\'.'
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
- 'SyntheticFlight requires BADA extension. Install with `pip install "pycontrails-bada @'
24
- ' git+ssh://git@github.com/contrailcirrus/pycontrails-bada.git"`'
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__)
@@ -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 = 6.0
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
- horizontal_resolution: float = 0.5
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
- met = standardize_variables(met, self.met_variables)
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 isinstance(self.source, MetDataset):
235
- # Overwrite horizontal resolution to match met
236
- longitude = self.source.data["longitude"].values
237
- if longitude.size > 1:
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 latitude.size > 1:
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 self.short_vars:
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 = {v.standard_name: v.short_name for v in self.met_variables}
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
- "NOx&inverse_EIs": params["nox_ei"],
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: Flight | None
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.advect_longitude(longitude_1, latitude_1, u_wind_1, dt)
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(-80, 80.000001, lat_step, dtype=float)
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 out
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.advect_longitude(
2058
- longitude=longitude, latitude=latitude, u_wind=u_wind, dt=dt
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.advect_longitude(
2093
- longitude=longitude_head, latitude=latitude_head, u_wind=u_wind_head, dt=dt_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
- latitude_head_t2 = geo.advect_latitude(latitude=latitude_head, v_wind=v_wind_head, dt=dt_head)
2096
-
2097
- longitude_tail_t2 = geo.advect_longitude(
2098
- longitude=longitude_tail, latitude=latitude_tail, u_wind=u_wind_tail, dt=dt_tail
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) -> 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,
@@ -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