pycontrails 0.54.0__cp312-cp312-win_amd64.whl → 0.54.2__cp312-cp312-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.

@@ -13,15 +13,20 @@ from __future__ import annotations
13
13
  import contextlib
14
14
  import hashlib
15
15
  import logging
16
+ import sys
16
17
  import warnings
17
18
  from datetime import datetime, timedelta
18
19
  from typing import Any
19
20
 
21
+ if sys.version_info >= (3, 12):
22
+ from typing import override
23
+ else:
24
+ from typing_extensions import override
25
+
20
26
  LOG = logging.getLogger(__name__)
21
27
 
22
28
  import pandas as pd
23
29
  import xarray as xr
24
- from overrides import overrides
25
30
 
26
31
  import pycontrails
27
32
  from pycontrails.core import cache
@@ -283,7 +288,7 @@ class HRESModelLevel(ECMWFAPI):
283
288
  """
284
289
  return []
285
290
 
286
- @overrides
291
+ @override
287
292
  def create_cachepath(self, t: datetime | pd.Timestamp) -> str:
288
293
  """Return cachepath to local HRES data file based on datetime.
289
294
 
@@ -316,13 +321,13 @@ class HRESModelLevel(ECMWFAPI):
316
321
 
317
322
  return self.cachestore.path(cache_path)
318
323
 
319
- @overrides
324
+ @override
320
325
  def download_dataset(self, times: list[datetime]) -> None:
321
326
  # will always submit a single MARS request since each forecast is a separate file on tape
322
327
  LOG.debug(f"Retrieving ERA5 data for times {times} from forecast {self.forecast_time}")
323
328
  self._download_convert_cache_handler(times)
324
329
 
325
- @overrides
330
+ @override
326
331
  def open_metdataset(
327
332
  self,
328
333
  dataset: xr.Dataset | None = None,
@@ -348,7 +353,7 @@ class HRESModelLevel(ECMWFAPI):
348
353
  self.set_metadata(mds)
349
354
  return mds
350
355
 
351
- @overrides
356
+ @override
352
357
  def set_metadata(self, ds: xr.Dataset | MetDataset) -> None:
353
358
  ds.attrs.update(
354
359
  provider="ECMWF", dataset="HRES", product="forecast", radiation_accumulated=True
@@ -4,16 +4,21 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  import pathlib
7
+ import sys
7
8
  import warnings
8
9
  from datetime import datetime
9
10
  from typing import Any
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
  LOG = logging.getLogger(__name__)
12
18
 
13
19
  import numpy as np
14
20
  import pandas as pd
15
21
  import xarray as xr
16
- from overrides import overrides
17
22
 
18
23
  from pycontrails.core import met
19
24
  from pycontrails.datalib._met_utils import metsource
@@ -119,7 +124,7 @@ class IFS(metsource.MetDataSource):
119
124
  """
120
125
  return None
121
126
 
122
- @overrides
127
+ @override
123
128
  def open_metdataset(
124
129
  self,
125
130
  dataset: xr.Dataset | None = None,
@@ -190,7 +195,7 @@ class IFS(metsource.MetDataSource):
190
195
  self.set_metadata(ds)
191
196
  return met.MetDataset(ds, **kwargs)
192
197
 
193
- @overrides
198
+ @override
194
199
  def set_metadata(self, ds: xr.Dataset | met.MetDataset) -> None:
195
200
  ds.attrs.update(
196
201
  provider="ECMWF",
@@ -198,15 +203,15 @@ class IFS(metsource.MetDataSource):
198
203
  product="forecast",
199
204
  )
200
205
 
201
- @overrides
206
+ @override
202
207
  def download_dataset(self, times: list[datetime]) -> None:
203
208
  raise NotImplementedError("IFS download is not supported")
204
209
 
205
- @overrides
210
+ @override
206
211
  def cache_dataset(self, dataset: xr.Dataset) -> None:
207
212
  raise NotImplementedError("IFS dataset caching not supported")
208
213
 
209
- @overrides
214
+ @override
210
215
  def create_cachepath(self, t: datetime) -> str:
211
216
  raise NotImplementedError("IFS download is not supported")
212
217
 
@@ -384,9 +384,10 @@ def ml_to_pl(
384
384
  lnsp : xr.DataArray
385
385
  Natural logarithm of surface pressure, [:math:`\ln(\text{Pa})`]. If provided,
386
386
  ``sp`` is ignored. At least one of ``lnsp`` or ``sp`` must be provided.
387
+ The chunking over dimensions in common with ``ds`` must be the same as ``ds``.
387
388
  sp : xr.DataArray
388
- Surface pressure, [:math:`\text{Pa}`].
389
- At least one of ``lnsp`` or ``sp`` must be provided.
389
+ Surface pressure, [:math:`\text{Pa}`]. At least one of ``lnsp`` or ``sp`` must be provided.
390
+ The chunking over dimensions in common with ``ds`` must be the same as ``ds``.
390
391
 
391
392
  Returns
392
393
  -------
@@ -9,18 +9,24 @@ References
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import contextlib
12
13
  import hashlib
13
14
  import logging
14
15
  import pathlib
16
+ import sys
15
17
  import warnings
16
18
  from collections.abc import Callable
17
19
  from datetime import datetime
18
20
  from typing import TYPE_CHECKING, Any
19
21
 
22
+ if sys.version_info >= (3, 12):
23
+ from typing import override
24
+ else:
25
+ from typing_extensions import override
26
+
20
27
  import numpy as np
21
28
  import pandas as pd
22
29
  import xarray as xr
23
- from overrides import overrides
24
30
 
25
31
  import pycontrails
26
32
  from pycontrails.core import cache, met
@@ -82,6 +88,9 @@ class GFSForecast(metsource.MetDataSource):
82
88
  show_progress : bool, optional
83
89
  Show progress when downloading files from GFS AWS Bucket.
84
90
  Defaults to False
91
+ cache_download: bool, optional
92
+ If True, cache downloaded grib files rather than storing them in a temporary file.
93
+ By default, False.
85
94
 
86
95
  Examples
87
96
  --------
@@ -116,7 +125,7 @@ class GFSForecast(metsource.MetDataSource):
116
125
  - `GFS Documentation <https://www.emc.ncep.noaa.gov/emc/pages/numerical_forecast_systems/gfs/documentation.php>`_
117
126
  """
118
127
 
119
- __slots__ = ("client", "grid", "cachestore", "show_progress", "forecast_time")
128
+ __slots__ = ("client", "grid", "cachestore", "show_progress", "forecast_time", "cache_download")
120
129
 
121
130
  #: S3 client for accessing GFS bucket
122
131
  client: botocore.client.S3
@@ -142,7 +151,8 @@ class GFSForecast(metsource.MetDataSource):
142
151
  forecast_time: DatetimeLike | None = None,
143
152
  cachestore: cache.CacheStore | None = __marker, # type: ignore[assignment]
144
153
  show_progress: bool = False,
145
- ):
154
+ cache_download: bool = False,
155
+ ) -> None:
146
156
  try:
147
157
  import boto3
148
158
  except ModuleNotFoundError as e:
@@ -169,6 +179,7 @@ class GFSForecast(metsource.MetDataSource):
169
179
  cachestore = cache.DiskCacheStore()
170
180
  self.cachestore = cachestore
171
181
  self.show_progress = show_progress
182
+ self.cache_download = cache_download
172
183
 
173
184
  if time is None and paths is None:
174
185
  raise ValueError("Time input is required when paths is None")
@@ -349,7 +360,7 @@ class GFSForecast(metsource.MetDataSource):
349
360
  forecast_hour = str(self.forecast_time.hour).zfill(2)
350
361
  return f"gfs.t{forecast_hour}z.pgrb2.{self._grid_string}.f{step_hour}"
351
362
 
352
- @overrides
363
+ @override
353
364
  def create_cachepath(self, t: datetime) -> str:
354
365
  if self.cachestore is None:
355
366
  raise ValueError("self.cachestore attribute must be defined to create cache path")
@@ -366,7 +377,7 @@ class GFSForecast(metsource.MetDataSource):
366
377
  # return cache path
367
378
  return self.cachestore.path(f"{datestr}-{step}-{suffix}.nc")
368
379
 
369
- @overrides
380
+ @override
370
381
  def download_dataset(self, times: list[datetime]) -> None:
371
382
  # get step relative to forecast forecast_time
372
383
  logger.debug(
@@ -377,7 +388,7 @@ class GFSForecast(metsource.MetDataSource):
377
388
  for t in times:
378
389
  self._download_file(t)
379
390
 
380
- @overrides
391
+ @override
381
392
  def cache_dataset(self, dataset: xr.Dataset) -> None:
382
393
  # if self.cachestore is None:
383
394
  # LOG.debug("Cache is turned off, skipping")
@@ -385,7 +396,7 @@ class GFSForecast(metsource.MetDataSource):
385
396
 
386
397
  raise NotImplementedError("GFS caching only implemented with download")
387
398
 
388
- @overrides
399
+ @override
389
400
  def open_metdataset(
390
401
  self,
391
402
  dataset: xr.Dataset | None = None,
@@ -429,7 +440,7 @@ class GFSForecast(metsource.MetDataSource):
429
440
  # run the same GFS-specific processing on the dataset
430
441
  return self._process_dataset(ds, **kwargs)
431
442
 
432
- @overrides
443
+ @override
433
444
  def set_metadata(self, ds: xr.Dataset | met.MetDataset) -> None:
434
445
  ds.attrs.update(
435
446
  provider="NCEP",
@@ -462,24 +473,30 @@ class GFSForecast(metsource.MetDataSource):
462
473
  filename = self.filename(t)
463
474
  aws_key = f"{self.forecast_path}/{filename}"
464
475
 
476
+ stack = contextlib.ExitStack()
477
+ if self.cache_download:
478
+ target = self.cachestore.path(aws_key.replace("/", "-"))
479
+ else:
480
+ target = stack.enter_context(temp.temp_file())
481
+
465
482
  # Hold downloaded file in named temp file
466
- with temp.temp_file() as temp_grib_filename:
483
+ with stack:
467
484
  # retrieve data from AWS S3
468
- logger.debug(f"Downloading GFS file {filename} from AWS bucket to {temp_grib_filename}")
469
- if self.show_progress:
470
- _download_with_progress(
471
- self.client, GFS_FORECAST_BUCKET, aws_key, temp_grib_filename, filename
472
- )
473
- else:
474
- self.client.download_file(
475
- Bucket=GFS_FORECAST_BUCKET, Key=aws_key, Filename=temp_grib_filename
476
- )
485
+ logger.debug(f"Downloading GFS file {filename} from AWS bucket to {target}")
486
+ if not self.cache_download or not self.cachestore.exists(target):
487
+ self._make_download(aws_key, target, filename)
477
488
 
478
- ds = self._open_gfs_dataset(temp_grib_filename, t)
489
+ ds = self._open_gfs_dataset(target, t)
479
490
 
480
491
  cache_path = self.create_cachepath(t)
481
492
  ds.to_netcdf(cache_path)
482
493
 
494
+ def _make_download(self, aws_key: str, target: str, filename: str) -> None:
495
+ if self.show_progress:
496
+ _download_with_progress(self.client, GFS_FORECAST_BUCKET, aws_key, target, filename)
497
+ else:
498
+ self.client.download_file(Bucket=GFS_FORECAST_BUCKET, Key=aws_key, Filename=target)
499
+
483
500
  def _open_gfs_dataset(self, filepath: str | pathlib.Path, t: datetime) -> xr.Dataset:
484
501
  """Open GFS grib file for one forecast timestep.
485
502
 
@@ -502,7 +519,7 @@ class GFSForecast(metsource.MetDataSource):
502
519
  step = pd.Timedelta(t - self.forecast_time) // pd.Timedelta(1, "h")
503
520
 
504
521
  # open file for each variable short name individually
505
- ds: xr.Dataset | None = None
522
+ da_dict = {}
506
523
  for variable in self.variables:
507
524
  # Radiation data is not available in the 0th step
508
525
  is_radiation_step_zero = step == 0 and variable in (
@@ -519,23 +536,24 @@ class GFSForecast(metsource.MetDataSource):
519
536
  else:
520
537
  v = variable
521
538
 
522
- tmpds = xr.open_dataset(
523
- filepath,
524
- filter_by_keys={"typeOfLevel": v.level_type, "shortName": v.short_name},
525
- engine="cfgrib",
526
- )
527
-
528
- if ds is None:
529
- ds = tmpds
530
- else:
531
- ds[v.short_name] = tmpds[v.short_name]
539
+ try:
540
+ da = xr.open_dataarray(
541
+ filepath,
542
+ filter_by_keys={"typeOfLevel": v.level_type, "shortName": v.short_name},
543
+ engine="cfgrib",
544
+ )
545
+ except ValueError as exc:
546
+ # To debug this situation, you can use:
547
+ # import cfgrib
548
+ # cfgrib.open_datasets(filepath)
549
+ msg = f"Variable {v.short_name} not found in {filepath}"
550
+ raise ValueError(msg) from exc
532
551
 
533
- # set all radiation data to np.nan in the 0th step
534
552
  if is_radiation_step_zero:
535
- ds = ds.rename({Visibility.short_name: variable.short_name})
536
- ds[variable.short_name] = np.nan
553
+ da = xr.full_like(da, np.nan) # set all radiation data to np.nan in the 0th step
554
+ da_dict[variable.short_name] = da
537
555
 
538
- assert ds is not None, "No variables were loaded from grib file"
556
+ ds = xr.Dataset(da_dict)
539
557
 
540
558
  # for pressure levels, need to rename "level" field and downselect
541
559
  if self.pressure_levels != [-1]:
@@ -43,7 +43,9 @@ CloudIceWaterMixingRatio = MetVariable(
43
43
 
44
44
 
45
45
  TOAUpwardShortwaveRadiation = MetVariable(
46
- short_name="uswrf",
46
+ # Note the variable in the GFS Grib file is "uswrf" for the "nominalTop" level
47
+ # eccodes > 2.38 rewrites to `suswrf` on loading
48
+ short_name="suswrf",
47
49
  standard_name="toa_upward_shortwave_flux",
48
50
  long_name="Top of atmosphere upward shortwave radiation",
49
51
  units="W m**-2",
@@ -56,7 +58,9 @@ TOAUpwardShortwaveRadiation = MetVariable(
56
58
  )
57
59
 
58
60
  TOAUpwardLongwaveRadiation = MetVariable(
59
- short_name="ulwrf",
61
+ # Note the variable in the GFS Grib file is "ulwrf" for the "nominalTop" level
62
+ # eccodes > 2.38 rewrites to `sulwrf` on loading
63
+ short_name="sulwrf",
60
64
  standard_name="toa_upward_longwave_flux",
61
65
  long_name="Top of atmosphere upward longwave radiation",
62
66
  units="W m**-2",
@@ -3,15 +3,20 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ import sys
6
7
  import warnings
7
8
  from collections.abc import Sequence
8
9
  from typing import Any, Literal, NoReturn, overload
9
10
 
11
+ if sys.version_info >= (3, 12):
12
+ from typing import override
13
+ else:
14
+ from typing_extensions import override
15
+
10
16
  import numpy as np
11
17
  import numpy.typing as npt
12
18
  import pandas as pd
13
19
  import xarray as xr
14
- from overrides import overrides
15
20
 
16
21
  from pycontrails.core import met_var
17
22
  from pycontrails.core.aircraft_performance import AircraftPerformance
@@ -1218,7 +1223,7 @@ class Cocip(Model):
1218
1223
 
1219
1224
  return self._downwash_contrail.filter(filt)
1220
1225
 
1221
- @overrides
1226
+ @override
1222
1227
  def _cleanup_indices(self) -> None:
1223
1228
  """Cleanup interpolation artifacts."""
1224
1229
 
@@ -1671,6 +1671,8 @@ def calc_evolve_one_step(
1671
1671
  segment_length_t1, segment_length_t2
1672
1672
  )
1673
1673
 
1674
+ dt = next_contrail["time"] - curr_contrail["time"]
1675
+
1674
1676
  sigma_yy_t2, sigma_zz_t2, sigma_yz_t2 = contrail_properties.plume_temporal_evolution(
1675
1677
  width_t1=width_t1,
1676
1678
  depth_t1=depth_t1,
@@ -1679,7 +1681,7 @@ def calc_evolve_one_step(
1679
1681
  diffuse_h_t1=diffuse_h_t1,
1680
1682
  diffuse_v_t1=diffuse_v_t1,
1681
1683
  seg_ratio=seg_ratio_t12,
1682
- dt=params["dt_integration"],
1684
+ dt=dt,
1683
1685
  max_depth=params["max_depth"],
1684
1686
  )
1685
1687
 
@@ -1716,7 +1718,7 @@ def calc_evolve_one_step(
1716
1718
  dn_dt_agg=dn_dt_agg,
1717
1719
  dn_dt_turb=dn_dt_turb,
1718
1720
  seg_ratio=seg_ratio_t12,
1719
- dt=params["dt_integration"],
1721
+ dt=dt,
1720
1722
  )
1721
1723
  next_contrail["n_ice_per_m"] = n_ice_per_m_t2
1722
1724
 
@@ -1737,7 +1739,7 @@ def calc_evolve_one_step(
1737
1739
  width_t1=width_t1,
1738
1740
  width_t2=width_t2,
1739
1741
  seg_length_t2=segment_length_t2,
1740
- dt=params["dt_integration"],
1742
+ dt=dt,
1741
1743
  )
1742
1744
  # NOTE: This will get masked below if `persistent` is False
1743
1745
  # That is, we are taking a right Riemann sum of a decreasing function, so we are
@@ -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],
@@ -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 (
@@ -20,6 +25,7 @@ from pycontrails.core.aircraft_performance import (
20
25
  AircraftPerformanceData,
21
26
  AircraftPerformanceParams,
22
27
  )
28
+ from pycontrails.core.fleet import Fleet
23
29
  from pycontrails.core.flight import Flight
24
30
  from pycontrails.core.met import MetDataset
25
31
  from pycontrails.core.met_var import AirTemperature, EastwardWind, NorthwardWind
@@ -115,13 +121,16 @@ class PSFlight(AircraftPerformance):
115
121
  raise KeyError(msg)
116
122
  return False
117
123
 
124
+ @overload
125
+ def eval(self, source: Fleet, **params: Any) -> Fleet: ...
126
+
118
127
  @overload
119
128
  def eval(self, source: Flight, **params: Any) -> Flight: ...
120
129
 
121
130
  @overload
122
131
  def eval(self, source: None = ..., **params: Any) -> NoReturn: ...
123
132
 
124
- @overrides
133
+ @override
125
134
  def eval(self, source: Flight | None = None, **params: Any) -> Flight:
126
135
  self.update_params(params)
127
136
  self.set_source(source)
@@ -130,12 +139,20 @@ class PSFlight(AircraftPerformance):
130
139
  self.set_source_met()
131
140
 
132
141
  # Calculate true airspeed if not included on source
133
- true_airspeed = self.ensure_true_airspeed_on_source().copy()
134
- true_airspeed[true_airspeed == 0.0] = np.nan
142
+ self.ensure_true_airspeed_on_source()
143
+
144
+ if isinstance(self.source, Fleet):
145
+ fls = [self._eval_flight(fl) for fl in self.source.to_flight_list()]
146
+ self.source = Fleet.from_seq(fls, attrs=self.source.attrs, broadcast_numeric=False)
147
+ return self.source
148
+
149
+ self.source = self._eval_flight(self.source)
150
+ return self.source
135
151
 
152
+ def _eval_flight(self, fl: Flight) -> Flight:
136
153
  # Ensure aircraft type is available
137
154
  try:
138
- aircraft_type = self.source.attrs["aircraft_type"]
155
+ aircraft_type = fl.attrs["aircraft_type"]
139
156
  except KeyError as exc:
140
157
  msg = "`aircraft_type` required on flight attrs"
141
158
  raise KeyError(msg) from exc
@@ -148,29 +165,32 @@ class PSFlight(AircraftPerformance):
148
165
  raise KeyError(msg) from exc
149
166
 
150
167
  # Set flight attributes based on engine, if they aren't already defined
151
- self.source.attrs.setdefault("aircraft_performance_model", self.name)
152
- self.source.attrs.setdefault("aircraft_type_ps", atyp_ps)
153
- self.source.attrs.setdefault("n_engine", aircraft_params.n_engine)
154
-
155
- self.source.attrs.setdefault("wingspan", aircraft_params.wing_span)
156
- self.source.attrs.setdefault("max_mach", aircraft_params.max_mach_num)
157
- self.source.attrs.setdefault("max_altitude", units.ft_to_m(aircraft_params.fl_max * 100.0))
158
- self.source.attrs.setdefault("n_engine", aircraft_params.n_engine)
159
-
160
- amass_oew = self.source.attrs.get("amass_oew", aircraft_params.amass_oew)
161
- amass_mtow = self.source.attrs.get("amass_mtow", aircraft_params.amass_mtow)
162
- amass_mpl = self.source.attrs.get("amass_mpl", aircraft_params.amass_mpl)
163
- load_factor = self.source.attrs.get("load_factor", DEFAULT_LOAD_FACTOR)
164
- takeoff_mass = self.source.attrs.get("takeoff_mass")
165
- q_fuel = self.source.fuel.q_fuel
168
+ fl.attrs.setdefault("aircraft_performance_model", self.name)
169
+ fl.attrs.setdefault("aircraft_type_ps", atyp_ps)
170
+ fl.attrs.setdefault("n_engine", aircraft_params.n_engine)
171
+
172
+ fl.attrs.setdefault("wingspan", aircraft_params.wing_span)
173
+ fl.attrs.setdefault("max_mach", aircraft_params.max_mach_num)
174
+ fl.attrs.setdefault("max_altitude", units.ft_to_m(aircraft_params.fl_max * 100.0))
175
+ fl.attrs.setdefault("n_engine", aircraft_params.n_engine)
176
+
177
+ amass_oew = fl.attrs.get("amass_oew", aircraft_params.amass_oew)
178
+ amass_mtow = fl.attrs.get("amass_mtow", aircraft_params.amass_mtow)
179
+ amass_mpl = fl.attrs.get("amass_mpl", aircraft_params.amass_mpl)
180
+ load_factor = fl.attrs.get("load_factor", DEFAULT_LOAD_FACTOR)
181
+ takeoff_mass = fl.attrs.get("takeoff_mass")
182
+ q_fuel = fl.fuel.q_fuel
183
+
184
+ true_airspeed = fl["true_airspeed"] # attached in PSFlight.eval
185
+ true_airspeed = np.where(true_airspeed == 0.0, np.nan, true_airspeed)
166
186
 
167
187
  # Run the simulation
168
188
  aircraft_performance = self.simulate_fuel_and_performance(
169
189
  aircraft_type=atyp_ps,
170
- altitude_ft=self.source.altitude_ft,
171
- time=self.source["time"],
190
+ altitude_ft=fl.altitude_ft,
191
+ time=fl["time"],
172
192
  true_airspeed=true_airspeed,
173
- air_temperature=self.source["air_temperature"],
193
+ air_temperature=fl["air_temperature"],
174
194
  aircraft_mass=self.get_source_param("aircraft_mass", None),
175
195
  thrust=self.get_source_param("thrust", None),
176
196
  engine_efficiency=self.get_source_param("engine_efficiency", None),
@@ -194,15 +214,15 @@ class PSFlight(AircraftPerformance):
194
214
  "thrust",
195
215
  "rocd",
196
216
  ):
197
- self.source.setdefault(var, getattr(aircraft_performance, var))
217
+ fl.setdefault(var, getattr(aircraft_performance, var))
198
218
 
199
219
  self._cleanup_indices()
200
220
 
201
- self.source.attrs["total_fuel_burn"] = np.nansum(aircraft_performance.fuel_burn).item()
221
+ fl.attrs["total_fuel_burn"] = np.nansum(aircraft_performance.fuel_burn).item()
202
222
 
203
- return self.source
223
+ return fl
204
224
 
205
- @overrides
225
+ @override
206
226
  def calculate_aircraft_performance(
207
227
  self,
208
228
  *,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pycontrails
3
- Version: 0.54.0
3
+ Version: 0.54.2
4
4
  Summary: Python library for modeling aviation climate impacts
5
5
  Author-email: Breakthrough Energy <py@contrails.org>
6
6
  License: Apache-2.0
@@ -29,10 +29,10 @@ License-File: LICENSE
29
29
  License-File: NOTICE
30
30
  Requires-Dist: dask>=2022.3
31
31
  Requires-Dist: numpy>=1.22
32
- Requires-Dist: overrides>=6.1
33
32
  Requires-Dist: pandas>=2.2
34
33
  Requires-Dist: scipy>=1.10
35
34
  Requires-Dist: xarray>=2022.3
35
+ Requires-Dist: typing-extensions>=4.5; python_version < "3.12"
36
36
  Provides-Extra: complete
37
37
  Requires-Dist: pycontrails[ecmwf,gcp,gfs,jupyter,pyproj,sat,vis,zarr]; extra == "complete"
38
38
  Provides-Extra: dev
@@ -67,7 +67,7 @@ Requires-Dist: sphinxext.opengraph>=0.8; extra == "docs"
67
67
  Provides-Extra: ecmwf
68
68
  Requires-Dist: cdsapi>=0.4; extra == "ecmwf"
69
69
  Requires-Dist: cfgrib>=0.9; extra == "ecmwf"
70
- Requires-Dist: eccodes>=1.4; extra == "ecmwf"
70
+ Requires-Dist: eccodes>=2.38; extra == "ecmwf"
71
71
  Requires-Dist: ecmwf-api-client>=1.6; extra == "ecmwf"
72
72
  Requires-Dist: netcdf4>=1.6.1; extra == "ecmwf"
73
73
  Requires-Dist: platformdirs>=3.0; extra == "ecmwf"
@@ -79,7 +79,8 @@ Requires-Dist: tqdm>=4.61; extra == "gcp"
79
79
  Provides-Extra: gfs
80
80
  Requires-Dist: boto3>=1.20; extra == "gfs"
81
81
  Requires-Dist: cfgrib>=0.9; extra == "gfs"
82
- Requires-Dist: eccodes>=1.4; extra == "gfs"
82
+ Requires-Dist: eccodes>=2.38; extra == "gfs"
83
+ Requires-Dist: netcdf4>=1.6.1; extra == "gfs"
83
84
  Requires-Dist: platformdirs>=3.0; extra == "gfs"
84
85
  Requires-Dist: tqdm>=4.61; extra == "gfs"
85
86
  Provides-Extra: jupyter