pycontrails 0.54.1__cp313-cp313-win_amd64.whl → 0.54.3__cp313-cp313-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pycontrails might be problematic. Click here for more details.
- pycontrails/_version.py +2 -2
- pycontrails/core/aircraft_performance.py +24 -5
- pycontrails/core/cache.py +14 -10
- pycontrails/core/fleet.py +22 -12
- pycontrails/core/flight.py +25 -15
- pycontrails/core/met.py +34 -22
- pycontrails/core/rgi_cython.cp313-win_amd64.pyd +0 -0
- pycontrails/core/vector.py +38 -38
- pycontrails/datalib/ecmwf/arco_era5.py +10 -5
- pycontrails/datalib/ecmwf/common.py +7 -2
- pycontrails/datalib/ecmwf/era5.py +9 -4
- pycontrails/datalib/ecmwf/era5_model_level.py +9 -5
- pycontrails/datalib/ecmwf/hres.py +12 -7
- pycontrails/datalib/ecmwf/hres_model_level.py +10 -5
- pycontrails/datalib/ecmwf/ifs.py +11 -6
- pycontrails/datalib/ecmwf/variables.py +1 -0
- pycontrails/datalib/gfs/gfs.py +52 -34
- pycontrails/datalib/gfs/variables.py +6 -2
- pycontrails/datalib/landsat.py +5 -8
- pycontrails/datalib/sentinel.py +7 -11
- pycontrails/ext/bada.py +3 -2
- pycontrails/ext/synthetic_flight.py +3 -2
- pycontrails/models/accf.py +40 -19
- pycontrails/models/apcemm/apcemm.py +2 -1
- pycontrails/models/cocip/cocip.py +8 -4
- pycontrails/models/cocipgrid/cocip_grid.py +25 -20
- pycontrails/models/dry_advection.py +50 -54
- pycontrails/models/humidity_scaling/humidity_scaling.py +12 -7
- pycontrails/models/ps_model/__init__.py +2 -1
- pycontrails/models/ps_model/ps_aircraft_params.py +3 -2
- pycontrails/models/ps_model/ps_grid.py +187 -1
- pycontrails/models/ps_model/ps_model.py +12 -10
- pycontrails/models/ps_model/ps_operational_limits.py +39 -52
- pycontrails/physics/geo.py +149 -0
- pycontrails/physics/jet.py +141 -11
- pycontrails/physics/static/iata-cargo-load-factors-20241115.csv +71 -0
- pycontrails/physics/static/iata-passenger-load-factors-20241115.csv +71 -0
- {pycontrails-0.54.1.dist-info → pycontrails-0.54.3.dist-info}/METADATA +12 -11
- {pycontrails-0.54.1.dist-info → pycontrails-0.54.3.dist-info}/RECORD +43 -41
- {pycontrails-0.54.1.dist-info → pycontrails-0.54.3.dist-info}/WHEEL +1 -1
- {pycontrails-0.54.1.dist-info → pycontrails-0.54.3.dist-info}/LICENSE +0 -0
- {pycontrails-0.54.1.dist-info → pycontrails-0.54.3.dist-info}/NOTICE +0 -0
- {pycontrails-0.54.1.dist-info → pycontrails-0.54.3.dist-info}/top_level.txt +0 -0
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",
|
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
|
|
@@ -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
|
|
|
@@ -2291,8 +2296,7 @@ def calc_timestep_contrail_evolution(
|
|
|
2291
2296
|
dt = time_2_array - time_1
|
|
2292
2297
|
|
|
2293
2298
|
# get new contrail location & segment properties after t_step
|
|
2294
|
-
longitude_2 = geo.
|
|
2295
|
-
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)
|
|
2296
2300
|
level_2 = geo.advect_level(level_1, vertical_velocity_1, rho_air_1, terminal_fall_speed_1, dt)
|
|
2297
2301
|
altitude_2 = units.pl_to_m(level_2)
|
|
2298
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,
|