pycontrails 0.54.0__cp310-cp310-macosx_11_0_arm64.whl → 0.54.2__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 +16 -5
- pycontrails/core/cache.py +14 -10
- pycontrails/core/fleet.py +27 -34
- pycontrails/core/flight.py +31 -91
- pycontrails/core/met.py +64 -67
- pycontrails/core/rgi_cython.cpython-310-darwin.so +0 -0
- pycontrails/core/vector.py +51 -82
- pycontrails/datalib/ecmwf/__init__.py +2 -2
- pycontrails/datalib/ecmwf/arco_era5.py +11 -6
- pycontrails/datalib/ecmwf/common.py +7 -2
- pycontrails/datalib/ecmwf/era5.py +10 -5
- pycontrails/datalib/ecmwf/era5_model_level.py +12 -8
- 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/model_levels.py +3 -2
- pycontrails/datalib/gfs/gfs.py +52 -34
- pycontrails/datalib/gfs/variables.py +6 -2
- pycontrails/models/cocip/cocip.py +7 -2
- pycontrails/models/cocipgrid/cocip_grid.py +5 -3
- pycontrails/models/humidity_scaling/humidity_scaling.py +12 -7
- pycontrails/models/ps_model/ps_model.py +47 -27
- {pycontrails-0.54.0.dist-info → pycontrails-0.54.2.dist-info}/METADATA +5 -4
- {pycontrails-0.54.0.dist-info → pycontrails-0.54.2.dist-info}/RECORD +29 -29
- {pycontrails-0.54.0.dist-info → pycontrails-0.54.2.dist-info}/WHEEL +1 -1
- {pycontrails-0.54.0.dist-info → pycontrails-0.54.2.dist-info}/LICENSE +0 -0
- {pycontrails-0.54.0.dist-info → pycontrails-0.54.2.dist-info}/NOTICE +0 -0
- {pycontrails-0.54.0.dist-info → pycontrails-0.54.2.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
pycontrails/datalib/ecmwf/ifs.py
CHANGED
|
@@ -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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
206
|
+
@override
|
|
202
207
|
def download_dataset(self, times: list[datetime]) -> None:
|
|
203
208
|
raise NotImplementedError("IFS download is not supported")
|
|
204
209
|
|
|
205
|
-
@
|
|
210
|
+
@override
|
|
206
211
|
def cache_dataset(self, dataset: xr.Dataset) -> None:
|
|
207
212
|
raise NotImplementedError("IFS dataset caching not supported")
|
|
208
213
|
|
|
209
|
-
@
|
|
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
|
-
|
|
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
|
-------
|
pycontrails/datalib/gfs/gfs.py
CHANGED
|
@@ -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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
|
483
|
+
with stack:
|
|
467
484
|
# retrieve data from AWS S3
|
|
468
|
-
logger.debug(f"Downloading GFS file {filename} from AWS bucket to {
|
|
469
|
-
if self.
|
|
470
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
536
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
-
@
|
|
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],
|
|
@@ -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
|
-
@
|
|
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
|
-
|
|
134
|
-
|
|
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 =
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
amass_oew =
|
|
161
|
-
amass_mtow =
|
|
162
|
-
amass_mpl =
|
|
163
|
-
load_factor =
|
|
164
|
-
takeoff_mass =
|
|
165
|
-
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=
|
|
171
|
-
time=
|
|
190
|
+
altitude_ft=fl.altitude_ft,
|
|
191
|
+
time=fl["time"],
|
|
172
192
|
true_airspeed=true_airspeed,
|
|
173
|
-
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
|
-
|
|
217
|
+
fl.setdefault(var, getattr(aircraft_performance, var))
|
|
198
218
|
|
|
199
219
|
self._cleanup_indices()
|
|
200
220
|
|
|
201
|
-
|
|
221
|
+
fl.attrs["total_fuel_burn"] = np.nansum(aircraft_performance.fuel_burn).item()
|
|
202
222
|
|
|
203
|
-
return
|
|
223
|
+
return fl
|
|
204
224
|
|
|
205
|
-
@
|
|
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.
|
|
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>=
|
|
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>=
|
|
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
|