pycontrails 0.53.0__cp313-cp313-macosx_10_13_x86_64.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/__init__.py +70 -0
- pycontrails/_version.py +16 -0
- pycontrails/core/__init__.py +30 -0
- pycontrails/core/aircraft_performance.py +641 -0
- pycontrails/core/airports.py +226 -0
- pycontrails/core/cache.py +881 -0
- pycontrails/core/coordinates.py +174 -0
- pycontrails/core/fleet.py +470 -0
- pycontrails/core/flight.py +2312 -0
- pycontrails/core/flightplan.py +220 -0
- pycontrails/core/fuel.py +140 -0
- pycontrails/core/interpolation.py +721 -0
- pycontrails/core/met.py +2833 -0
- pycontrails/core/met_var.py +307 -0
- pycontrails/core/models.py +1181 -0
- pycontrails/core/polygon.py +549 -0
- pycontrails/core/rgi_cython.cpython-313-darwin.so +0 -0
- pycontrails/core/vector.py +2191 -0
- pycontrails/datalib/__init__.py +12 -0
- pycontrails/datalib/_leo_utils/search.py +250 -0
- pycontrails/datalib/_leo_utils/static/bq_roi_query.sql +6 -0
- pycontrails/datalib/_leo_utils/vis.py +59 -0
- pycontrails/datalib/_met_utils/metsource.py +743 -0
- pycontrails/datalib/ecmwf/__init__.py +53 -0
- pycontrails/datalib/ecmwf/arco_era5.py +527 -0
- pycontrails/datalib/ecmwf/common.py +109 -0
- pycontrails/datalib/ecmwf/era5.py +538 -0
- pycontrails/datalib/ecmwf/era5_model_level.py +482 -0
- pycontrails/datalib/ecmwf/hres.py +782 -0
- pycontrails/datalib/ecmwf/hres_model_level.py +495 -0
- pycontrails/datalib/ecmwf/ifs.py +284 -0
- pycontrails/datalib/ecmwf/model_levels.py +79 -0
- pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
- pycontrails/datalib/ecmwf/variables.py +256 -0
- pycontrails/datalib/gfs/__init__.py +28 -0
- pycontrails/datalib/gfs/gfs.py +646 -0
- pycontrails/datalib/gfs/variables.py +100 -0
- pycontrails/datalib/goes.py +772 -0
- pycontrails/datalib/landsat.py +568 -0
- pycontrails/datalib/sentinel.py +512 -0
- pycontrails/datalib/spire.py +739 -0
- pycontrails/ext/bada.py +41 -0
- pycontrails/ext/cirium.py +14 -0
- pycontrails/ext/empirical_grid.py +140 -0
- pycontrails/ext/synthetic_flight.py +426 -0
- pycontrails/models/__init__.py +1 -0
- pycontrails/models/accf.py +406 -0
- pycontrails/models/apcemm/__init__.py +8 -0
- pycontrails/models/apcemm/apcemm.py +983 -0
- pycontrails/models/apcemm/inputs.py +226 -0
- pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
- pycontrails/models/apcemm/utils.py +437 -0
- pycontrails/models/cocip/__init__.py +29 -0
- pycontrails/models/cocip/cocip.py +2617 -0
- pycontrails/models/cocip/cocip_params.py +299 -0
- pycontrails/models/cocip/cocip_uncertainty.py +285 -0
- pycontrails/models/cocip/contrail_properties.py +1517 -0
- pycontrails/models/cocip/output_formats.py +2261 -0
- pycontrails/models/cocip/radiative_forcing.py +1262 -0
- pycontrails/models/cocip/radiative_heating.py +520 -0
- pycontrails/models/cocip/unterstrasser_wake_vortex.py +403 -0
- pycontrails/models/cocip/wake_vortex.py +396 -0
- pycontrails/models/cocip/wind_shear.py +120 -0
- pycontrails/models/cocipgrid/__init__.py +9 -0
- pycontrails/models/cocipgrid/cocip_grid.py +2573 -0
- pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
- pycontrails/models/dry_advection.py +486 -0
- pycontrails/models/emissions/__init__.py +21 -0
- pycontrails/models/emissions/black_carbon.py +594 -0
- pycontrails/models/emissions/emissions.py +1353 -0
- pycontrails/models/emissions/ffm2.py +336 -0
- pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
- pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
- pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
- pycontrails/models/humidity_scaling/__init__.py +37 -0
- pycontrails/models/humidity_scaling/humidity_scaling.py +1025 -0
- pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
- pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
- pycontrails/models/issr.py +210 -0
- pycontrails/models/pcc.py +327 -0
- pycontrails/models/pcr.py +154 -0
- pycontrails/models/ps_model/__init__.py +17 -0
- pycontrails/models/ps_model/ps_aircraft_params.py +376 -0
- pycontrails/models/ps_model/ps_grid.py +505 -0
- pycontrails/models/ps_model/ps_model.py +1017 -0
- pycontrails/models/ps_model/ps_operational_limits.py +540 -0
- pycontrails/models/ps_model/static/ps-aircraft-params-20240524.csv +68 -0
- pycontrails/models/ps_model/static/ps-synonym-list-20240524.csv +103 -0
- pycontrails/models/sac.py +459 -0
- pycontrails/models/tau_cirrus.py +168 -0
- pycontrails/physics/__init__.py +1 -0
- pycontrails/physics/constants.py +116 -0
- pycontrails/physics/geo.py +989 -0
- pycontrails/physics/jet.py +837 -0
- pycontrails/physics/thermo.py +451 -0
- pycontrails/physics/units.py +472 -0
- pycontrails/py.typed +0 -0
- pycontrails/utils/__init__.py +1 -0
- pycontrails/utils/dependencies.py +66 -0
- pycontrails/utils/iteration.py +13 -0
- pycontrails/utils/json.py +188 -0
- pycontrails/utils/temp.py +50 -0
- pycontrails/utils/types.py +165 -0
- pycontrails-0.53.0.dist-info/LICENSE +178 -0
- pycontrails-0.53.0.dist-info/METADATA +181 -0
- pycontrails-0.53.0.dist-info/NOTICE +43 -0
- pycontrails-0.53.0.dist-info/RECORD +109 -0
- pycontrails-0.53.0.dist-info/WHEEL +5 -0
- pycontrails-0.53.0.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"""Model-level ERA5 data access.
|
|
2
|
+
|
|
3
|
+
This module supports
|
|
4
|
+
|
|
5
|
+
- Retrieving model-level ERA5 data by submitting MARS requests through the Copernicus CDS.
|
|
6
|
+
- Processing retrieved GRIB files to produce netCDF files on target pressure levels.
|
|
7
|
+
- Local caching of processed netCDF files.
|
|
8
|
+
- Opening processed and cached files as a :class:`pycontrails.MetDataset` object.
|
|
9
|
+
|
|
10
|
+
Consider using :class:`pycontrails.datalib.ecmwf.ARCOERA5`
|
|
11
|
+
to access model-level data from the nominal ERA5 reanalysis between 1959 and 2022.
|
|
12
|
+
:class:`pycontrails.datalib.ecmwf.ARCOERA5` accesses data through Google's
|
|
13
|
+
`Analysis-Ready, Cloud Optimized ERA5 dataset <https://cloud.google.com/storage/docs/public-datasets/era5>`_
|
|
14
|
+
and has lower latency than this module, which retrieves data from the
|
|
15
|
+
`Copernicus Climate Data Store <https://cds.climate.copernicus.eu/#!/home>`_.
|
|
16
|
+
This module must be used to retrieve model-level data from ERA5 ensemble members
|
|
17
|
+
or for more recent dates.
|
|
18
|
+
|
|
19
|
+
This module requires the following additional dependency:
|
|
20
|
+
|
|
21
|
+
- `metview (binaries and python bindings) <https://metview.readthedocs.io/en/latest/python.html>`_
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import collections
|
|
27
|
+
import contextlib
|
|
28
|
+
import hashlib
|
|
29
|
+
import logging
|
|
30
|
+
import os
|
|
31
|
+
import warnings
|
|
32
|
+
from datetime import datetime
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
from overrides import overrides
|
|
36
|
+
|
|
37
|
+
LOG = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
import pandas as pd
|
|
40
|
+
import xarray as xr
|
|
41
|
+
|
|
42
|
+
import pycontrails
|
|
43
|
+
from pycontrails.core import cache
|
|
44
|
+
from pycontrails.core.met import MetDataset, MetVariable
|
|
45
|
+
from pycontrails.datalib._met_utils import metsource
|
|
46
|
+
from pycontrails.datalib.ecmwf.common import ECMWFAPI, CDSCredentialsNotFound
|
|
47
|
+
from pycontrails.datalib.ecmwf.model_levels import pressure_levels_at_model_levels
|
|
48
|
+
from pycontrails.datalib.ecmwf.variables import MODEL_LEVEL_VARIABLES
|
|
49
|
+
from pycontrails.utils import dependencies, temp
|
|
50
|
+
|
|
51
|
+
ALL_ENSEMBLE_MEMBERS = list(range(10))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ERA5ModelLevel(ECMWFAPI):
|
|
55
|
+
"""Class to support model-level ERA5 data access, download, and organization.
|
|
56
|
+
|
|
57
|
+
The interface is similar to :class:`pycontrails.datalib.ecmwf.ERA5`, which downloads pressure-level
|
|
58
|
+
with much lower vertical resolution.
|
|
59
|
+
|
|
60
|
+
Requires account with
|
|
61
|
+
`Copernicus Data Portal <https://cds.climate.copernicus.eu/cdsapp#!/home>`_
|
|
62
|
+
and local credentials.
|
|
63
|
+
|
|
64
|
+
API credentials can be stored in a ``~/.cdsapirc`` file
|
|
65
|
+
or as ``CDSAPI_URL`` and ``CDSAPI_KEY`` environment variables.
|
|
66
|
+
|
|
67
|
+
export CDSAPI_URL=...
|
|
68
|
+
export CDSAPI_KEY=...
|
|
69
|
+
|
|
70
|
+
Credentials can also be provided directly ``url`` and ``key`` keyword args.
|
|
71
|
+
|
|
72
|
+
See `cdsapi <https://github.com/ecmwf/cdsapi>`_ documentation
|
|
73
|
+
for more information.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
time : metsource.TimeInput | None
|
|
78
|
+
The time range for data retrieval, either a single datetime or (start, end) datetime range.
|
|
79
|
+
Input must be datetime-like or tuple of datetime-like
|
|
80
|
+
(:py:class:`datetime.datetime`, :class:`pandas.Timestamp`, :class:`numpy.datetime64`)
|
|
81
|
+
specifying the (start, end) of the date range, inclusive.
|
|
82
|
+
GRIB files will be downloaded from CDS in chunks no larger than 1 month
|
|
83
|
+
for the nominal reanalysis and no larger than 1 day for ensemble members.
|
|
84
|
+
This ensures that exactly one request is submitted per file on tape accessed.
|
|
85
|
+
If None, ``paths`` must be defined and all time coordinates will be loaded from files.
|
|
86
|
+
variables : metsource.VariableInput
|
|
87
|
+
Variable name (i.e. "t", "air_temperature", ["air_temperature, specific_humidity"])
|
|
88
|
+
pressure_levels : metsource.PressureLevelInput, optional
|
|
89
|
+
Pressure levels for data, in hPa (mbar).
|
|
90
|
+
To download surface-level parameters, use :class:`pycontrails.datalib.ecmwf.ERA5`.
|
|
91
|
+
Defaults to pressure levels that match model levels at a nominal surface pressure.
|
|
92
|
+
timestep_freq : str, optional
|
|
93
|
+
Manually set the timestep interval within the bounds defined by :attr:`time`.
|
|
94
|
+
Supports any string that can be passed to ``pd.date_range(freq=...)``.
|
|
95
|
+
By default, this is set to "1h" for reanalysis products and "3h" for ensemble products.
|
|
96
|
+
product_type : str, optional
|
|
97
|
+
Product type, one of "reanalysis" and "ensemble_members". Unlike
|
|
98
|
+
:class:`pycontrails.datalib.ecmwf.ERA5`, this class does not support direct access to the
|
|
99
|
+
ensemble mean and spread, which are not available on model levels.
|
|
100
|
+
grid : float, optional
|
|
101
|
+
Specify latitude/longitude grid spacing in data.
|
|
102
|
+
By default, this is set to 0.25 for reanalysis products and 0.5 for ensemble products.
|
|
103
|
+
levels : list[int], optional
|
|
104
|
+
Specify ECMWF model levels to include in MARS requests.
|
|
105
|
+
By default, this is set to include all model levels.
|
|
106
|
+
ensemble_members : list[int], optional
|
|
107
|
+
Specify ensemble members to include.
|
|
108
|
+
Valid only when the product type is "ensemble_members".
|
|
109
|
+
By default, includes every available ensemble member.
|
|
110
|
+
cachestore : cache.CacheStore | None, optional
|
|
111
|
+
Cache data store for staging processed netCDF files.
|
|
112
|
+
Defaults to :class:`pycontrails.core.cache.DiskCacheStore`.
|
|
113
|
+
If None, cache is turned off.
|
|
114
|
+
cache_grib: bool, optional
|
|
115
|
+
If True, cache downloaded GRIB files rather than storing them in a temporary file.
|
|
116
|
+
By default, False.
|
|
117
|
+
url : str
|
|
118
|
+
Override `cdsapi <https://github.com/ecmwf/cdsapi>`_ url
|
|
119
|
+
key : str
|
|
120
|
+
Override `cdsapi <https://github.com/ecmwf/cdsapi>`_ key
|
|
121
|
+
""" # noqa: E501
|
|
122
|
+
|
|
123
|
+
__marker = object()
|
|
124
|
+
|
|
125
|
+
def __init__(
|
|
126
|
+
self,
|
|
127
|
+
time: metsource.TimeInput,
|
|
128
|
+
variables: metsource.VariableInput,
|
|
129
|
+
pressure_levels: metsource.PressureLevelInput | None = None,
|
|
130
|
+
timestep_freq: str | None = None,
|
|
131
|
+
product_type: str = "reanalysis",
|
|
132
|
+
grid: float | None = None,
|
|
133
|
+
levels: list[int] | None = None,
|
|
134
|
+
ensemble_members: list[int] | None = None,
|
|
135
|
+
cachestore: cache.CacheStore = __marker, # type: ignore[assignment]
|
|
136
|
+
n_jobs: int = 1,
|
|
137
|
+
cache_grib: bool = False,
|
|
138
|
+
url: str | None = None,
|
|
139
|
+
key: str | None = None,
|
|
140
|
+
) -> None:
|
|
141
|
+
|
|
142
|
+
self.cachestore = cache.DiskCacheStore() if cachestore is self.__marker else cachestore
|
|
143
|
+
self.cache_grib = cache_grib
|
|
144
|
+
|
|
145
|
+
self.paths = None
|
|
146
|
+
|
|
147
|
+
self.url = url or os.getenv("CDSAPI_URL")
|
|
148
|
+
self.key = key or os.getenv("CDSAPI_KEY")
|
|
149
|
+
|
|
150
|
+
supported = ("reanalysis", "ensemble_members")
|
|
151
|
+
if product_type not in supported:
|
|
152
|
+
msg = (
|
|
153
|
+
f"Unknown product_type {product_type}. "
|
|
154
|
+
f"Currently support product types: {', '.join(supported)}"
|
|
155
|
+
)
|
|
156
|
+
raise ValueError(msg)
|
|
157
|
+
self.product_type = product_type
|
|
158
|
+
|
|
159
|
+
if product_type == "reanalysis" and ensemble_members:
|
|
160
|
+
msg = "No ensemble members available for reanalysis product type."
|
|
161
|
+
raise ValueError(msg)
|
|
162
|
+
if product_type == "ensemble_members" and not ensemble_members:
|
|
163
|
+
ensemble_members = ALL_ENSEMBLE_MEMBERS
|
|
164
|
+
self.ensemble_members = ensemble_members
|
|
165
|
+
|
|
166
|
+
if grid is None:
|
|
167
|
+
grid = 0.25 if product_type == "reanalysis" else 0.5
|
|
168
|
+
else:
|
|
169
|
+
grid_min = 0.25 if product_type == "reanalysis" else 0.5
|
|
170
|
+
if grid < grid_min:
|
|
171
|
+
msg = (
|
|
172
|
+
f"The highest resolution available is {grid_min} degrees. "
|
|
173
|
+
f"Your downloaded data will have resolution {grid}, but it is a "
|
|
174
|
+
f"reinterpolation of the {grid_min} degree data. The same interpolation can be "
|
|
175
|
+
"achieved directly with xarray."
|
|
176
|
+
)
|
|
177
|
+
warnings.warn(msg)
|
|
178
|
+
self.grid = grid
|
|
179
|
+
|
|
180
|
+
if levels is None:
|
|
181
|
+
levels = list(range(1, 138))
|
|
182
|
+
if min(levels) < 1 or max(levels) > 137:
|
|
183
|
+
msg = "Retrieval levels must be between 1 and 137, inclusive."
|
|
184
|
+
raise ValueError(msg)
|
|
185
|
+
self.levels = levels
|
|
186
|
+
|
|
187
|
+
datasource_timestep_freq = "1h" if product_type == "reanalysis" else "3h"
|
|
188
|
+
if timestep_freq is None:
|
|
189
|
+
timestep_freq = datasource_timestep_freq
|
|
190
|
+
if not metsource.validate_timestep_freq(timestep_freq, datasource_timestep_freq):
|
|
191
|
+
msg = (
|
|
192
|
+
f"Product {self.product_type} has timestep frequency of {datasource_timestep_freq} "
|
|
193
|
+
f"and cannot support requested timestep frequency of {timestep_freq}."
|
|
194
|
+
)
|
|
195
|
+
raise ValueError(msg)
|
|
196
|
+
|
|
197
|
+
self.timesteps = metsource.parse_timesteps(time, freq=timestep_freq)
|
|
198
|
+
if pressure_levels is None:
|
|
199
|
+
pressure_levels = pressure_levels_at_model_levels(20_000.0, 50_000.0)
|
|
200
|
+
self.pressure_levels = metsource.parse_pressure_levels(pressure_levels)
|
|
201
|
+
self.variables = metsource.parse_variables(variables, self.pressure_level_variables)
|
|
202
|
+
|
|
203
|
+
def __repr__(self) -> str:
|
|
204
|
+
base = super().__repr__()
|
|
205
|
+
return f"{base}\n\tDataset: {self.dataset}\n\tProduct type: {self.product_type}"
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def pressure_level_variables(self) -> list[MetVariable]:
|
|
209
|
+
"""ECMWF pressure level parameters available on model levels.
|
|
210
|
+
|
|
211
|
+
Returns
|
|
212
|
+
-------
|
|
213
|
+
list[MetVariable]
|
|
214
|
+
List of MetVariable available in datasource
|
|
215
|
+
"""
|
|
216
|
+
return MODEL_LEVEL_VARIABLES
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def single_level_variables(self) -> list[MetVariable]:
|
|
220
|
+
"""ECMWF single-level parameters available on model levels.
|
|
221
|
+
|
|
222
|
+
Returns
|
|
223
|
+
-------
|
|
224
|
+
list[MetVariable]
|
|
225
|
+
Always returns an empty list.
|
|
226
|
+
To access single-level variables, used :class:`pycontrails.datalib.ecmwf.ERA5`.
|
|
227
|
+
"""
|
|
228
|
+
return []
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def dataset(self) -> str:
|
|
232
|
+
"""Select dataset for downloading model-level data.
|
|
233
|
+
|
|
234
|
+
Always returns "reanalysis-era5-complete".
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
str
|
|
239
|
+
Model-level ERA5 dataset name in CDS
|
|
240
|
+
"""
|
|
241
|
+
return "reanalysis-era5-complete"
|
|
242
|
+
|
|
243
|
+
@overrides
|
|
244
|
+
def create_cachepath(self, t: datetime | pd.Timestamp) -> str:
|
|
245
|
+
"""Return cachepath to local ERA5 data file based on datetime.
|
|
246
|
+
|
|
247
|
+
This uniquely defines a cached data file with class parameters.
|
|
248
|
+
|
|
249
|
+
Parameters
|
|
250
|
+
----------
|
|
251
|
+
t : datetime | pd.Timestamp
|
|
252
|
+
Datetime of datafile
|
|
253
|
+
|
|
254
|
+
Returns
|
|
255
|
+
-------
|
|
256
|
+
str
|
|
257
|
+
Path to local ERA5 data file
|
|
258
|
+
"""
|
|
259
|
+
if self.cachestore is None:
|
|
260
|
+
msg = "Cachestore is required to create cache path"
|
|
261
|
+
raise ValueError(msg)
|
|
262
|
+
|
|
263
|
+
string = (
|
|
264
|
+
f"{t:%Y%m%d%H}-"
|
|
265
|
+
f"{'.'.join(str(p) for p in self.pressure_levels)}-"
|
|
266
|
+
f"{'.'.join(sorted(self.variable_shortnames))}-"
|
|
267
|
+
f"{self.grid}"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
name = hashlib.md5(string.encode()).hexdigest()
|
|
271
|
+
cache_path = f"era5ml-{name}.nc"
|
|
272
|
+
|
|
273
|
+
return self.cachestore.path(cache_path)
|
|
274
|
+
|
|
275
|
+
@overrides
|
|
276
|
+
def download_dataset(self, times: list[datetime]) -> None:
|
|
277
|
+
|
|
278
|
+
# group data to request by month (nominal) or by day (ensemble)
|
|
279
|
+
requests: dict[datetime, list[datetime]] = collections.defaultdict(list)
|
|
280
|
+
for t in times:
|
|
281
|
+
request = (
|
|
282
|
+
datetime(t.year, t.month, 1)
|
|
283
|
+
if self.product_type == "reanalysis"
|
|
284
|
+
else datetime(t.year, t.month, t.day)
|
|
285
|
+
)
|
|
286
|
+
requests[request].append(t)
|
|
287
|
+
|
|
288
|
+
# retrieve and process data for each request
|
|
289
|
+
LOG.debug(f"Retrieving ERA5 data for times {times} in {len(requests)} request(s)")
|
|
290
|
+
for times_in_request in requests.values():
|
|
291
|
+
self._download_convert_cache_handler(times_in_request)
|
|
292
|
+
|
|
293
|
+
@overrides
|
|
294
|
+
def open_metdataset(
|
|
295
|
+
self,
|
|
296
|
+
dataset: xr.Dataset | None = None,
|
|
297
|
+
xr_kwargs: dict[str, Any] | None = None,
|
|
298
|
+
**kwargs: Any,
|
|
299
|
+
) -> MetDataset:
|
|
300
|
+
|
|
301
|
+
if dataset:
|
|
302
|
+
msg = "Parameter 'dataset' is not supported for Model-level ERA5 data"
|
|
303
|
+
raise ValueError(msg)
|
|
304
|
+
|
|
305
|
+
if self.cachestore is None:
|
|
306
|
+
msg = "Cachestore is required to download data"
|
|
307
|
+
raise ValueError(msg)
|
|
308
|
+
|
|
309
|
+
xr_kwargs = xr_kwargs or {}
|
|
310
|
+
self.download(**xr_kwargs)
|
|
311
|
+
|
|
312
|
+
disk_cachepaths = [self.cachestore.get(f) for f in self._cachepaths]
|
|
313
|
+
ds = self.open_dataset(disk_cachepaths, **xr_kwargs)
|
|
314
|
+
|
|
315
|
+
mds = self._process_dataset(ds, **kwargs)
|
|
316
|
+
|
|
317
|
+
self.set_metadata(mds)
|
|
318
|
+
return mds
|
|
319
|
+
|
|
320
|
+
@overrides
|
|
321
|
+
def set_metadata(self, ds: xr.Dataset | MetDataset) -> None:
|
|
322
|
+
if self.product_type == "reanalysis":
|
|
323
|
+
product = "reanalysis"
|
|
324
|
+
elif self.product_type == "ensemble_members":
|
|
325
|
+
product = "ensemble"
|
|
326
|
+
else:
|
|
327
|
+
msg = f"Unknown product type {self.product_type}"
|
|
328
|
+
raise ValueError(msg)
|
|
329
|
+
|
|
330
|
+
ds.attrs.update(
|
|
331
|
+
provider="ECMWF",
|
|
332
|
+
dataset="ERA5",
|
|
333
|
+
product=product,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def mars_request(self, times: list[datetime]) -> dict[str, str]:
|
|
337
|
+
"""Generate MARS request for specific list of times.
|
|
338
|
+
|
|
339
|
+
Parameters
|
|
340
|
+
----------
|
|
341
|
+
times : list[datetime]
|
|
342
|
+
Times included in MARS request.
|
|
343
|
+
|
|
344
|
+
Returns
|
|
345
|
+
-------
|
|
346
|
+
dict[str, str]:
|
|
347
|
+
MARS request for submission to Copernicus CDS.
|
|
348
|
+
"""
|
|
349
|
+
unique_dates = set(t.strftime("%Y-%m-%d") for t in times)
|
|
350
|
+
unique_times = set(t.strftime("%H:%M:%S") for t in times)
|
|
351
|
+
# param 152 = log surface pressure, needed for metview level conversion
|
|
352
|
+
grib_params = set((*self.variable_ecmwfids, 152))
|
|
353
|
+
common = {
|
|
354
|
+
"class": "ea",
|
|
355
|
+
"date": "/".join(sorted(unique_dates)),
|
|
356
|
+
"expver": "1",
|
|
357
|
+
"levelist": "/".join(str(lev) for lev in sorted(self.levels)),
|
|
358
|
+
"levtype": "ml",
|
|
359
|
+
"param": "/".join(str(p) for p in sorted(grib_params)),
|
|
360
|
+
"time": "/".join(sorted(unique_times)),
|
|
361
|
+
"type": "an",
|
|
362
|
+
"grid": f"{self.grid}/{self.grid}",
|
|
363
|
+
}
|
|
364
|
+
if self.product_type == "reanalysis":
|
|
365
|
+
specific = {"stream": "oper"}
|
|
366
|
+
elif self.product_type == "ensemble_members":
|
|
367
|
+
specific = {"stream": "enda"}
|
|
368
|
+
if self.ensemble_members is not None: # always defined; checked to satisfy mypy
|
|
369
|
+
specific |= {"number": "/".join(str(n) for n in self.ensemble_members)}
|
|
370
|
+
return common | specific
|
|
371
|
+
|
|
372
|
+
def _set_cds(self) -> None:
|
|
373
|
+
"""Set the cdsapi.Client instance."""
|
|
374
|
+
try:
|
|
375
|
+
import cdsapi
|
|
376
|
+
except ModuleNotFoundError as e:
|
|
377
|
+
dependencies.raise_module_not_found_error(
|
|
378
|
+
name="ERA5ModelLevel._set_cds method",
|
|
379
|
+
package_name="cdsapi",
|
|
380
|
+
module_not_found_error=e,
|
|
381
|
+
pycontrails_optional_package="ecmwf",
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
self.cds = cdsapi.Client(url=self.url, key=self.key)
|
|
386
|
+
# cdsapi throws base-level Exception
|
|
387
|
+
except Exception as err:
|
|
388
|
+
raise CDSCredentialsNotFound from err
|
|
389
|
+
|
|
390
|
+
def _download_convert_cache_handler(
|
|
391
|
+
self,
|
|
392
|
+
times: list[datetime],
|
|
393
|
+
) -> None:
|
|
394
|
+
"""Download, convert, and cache ERA5 model level data.
|
|
395
|
+
|
|
396
|
+
This function builds a MARS request and retrieves a single GRIB file.
|
|
397
|
+
The calling function should ensure that all times will be contained
|
|
398
|
+
in a single file on tape in the MARS archive.
|
|
399
|
+
|
|
400
|
+
Because MARS requests treat dates and times as separate dimensions,
|
|
401
|
+
retrieved data will include the Cartesian product of all unique
|
|
402
|
+
dates and times in the list of specified times.
|
|
403
|
+
|
|
404
|
+
After retrieval, this function processes the GRIB file
|
|
405
|
+
to produce the dataset specified by class attributes.
|
|
406
|
+
|
|
407
|
+
Parameters
|
|
408
|
+
----------
|
|
409
|
+
times : list[datetime]
|
|
410
|
+
Times to download in a single MARS request.
|
|
411
|
+
|
|
412
|
+
Notes
|
|
413
|
+
-----
|
|
414
|
+
This function depends on `metview <https://metview.readthedocs.io/en/latest/python.html>`_
|
|
415
|
+
python bindings and binaries.
|
|
416
|
+
|
|
417
|
+
The lifetime of the metview import must last until processed datasets are cached
|
|
418
|
+
to avoid premature deletion of metview temporary files.
|
|
419
|
+
"""
|
|
420
|
+
try:
|
|
421
|
+
import metview as mv
|
|
422
|
+
except ModuleNotFoundError as exc:
|
|
423
|
+
dependencies.raise_module_not_found_error(
|
|
424
|
+
"model_level.grib_to_dataset function",
|
|
425
|
+
package_name="metview",
|
|
426
|
+
module_not_found_error=exc,
|
|
427
|
+
extra="See https://metview.readthedocs.io/en/latest/install.html for instructions.",
|
|
428
|
+
)
|
|
429
|
+
except ImportError as exc:
|
|
430
|
+
msg = "Failed to import metview"
|
|
431
|
+
raise ImportError(msg) from exc
|
|
432
|
+
|
|
433
|
+
if self.cachestore is None:
|
|
434
|
+
msg = "Cachestore is required to download and cache data"
|
|
435
|
+
raise ValueError(msg)
|
|
436
|
+
|
|
437
|
+
stack = contextlib.ExitStack()
|
|
438
|
+
request = self.mars_request(times)
|
|
439
|
+
|
|
440
|
+
if not self.cache_grib:
|
|
441
|
+
target = stack.enter_context(temp.temp_file())
|
|
442
|
+
else:
|
|
443
|
+
request_str = ";".join(f"{p}:{request[p]}" for p in sorted(request.keys()))
|
|
444
|
+
name = hashlib.md5(request_str.encode()).hexdigest()
|
|
445
|
+
target = self.cachestore.path(f"era5ml-{name}.grib")
|
|
446
|
+
|
|
447
|
+
with stack:
|
|
448
|
+
if not self.cache_grib or not self.cachestore.exists(target):
|
|
449
|
+
if not hasattr(self, "cds"):
|
|
450
|
+
self._set_cds()
|
|
451
|
+
self.cds.retrieve("reanalysis-era5-complete", request, target)
|
|
452
|
+
|
|
453
|
+
# Read contents of GRIB file as metview Fieldset
|
|
454
|
+
LOG.debug("Opening GRIB file")
|
|
455
|
+
fs_ml = mv.read(target)
|
|
456
|
+
|
|
457
|
+
# reduce memory overhead by cacheing one timestep at a time
|
|
458
|
+
for time in times:
|
|
459
|
+
fs_pl = mv.Fieldset()
|
|
460
|
+
dimensions = self.ensemble_members if self.ensemble_members else [-1]
|
|
461
|
+
for ens in dimensions:
|
|
462
|
+
date = time.strftime("%Y%m%d")
|
|
463
|
+
t = time.strftime("%H%M")
|
|
464
|
+
selection = dict(date=date, time=t)
|
|
465
|
+
if ens >= 0:
|
|
466
|
+
selection |= dict(number=str(ens))
|
|
467
|
+
|
|
468
|
+
lnsp = fs_ml.select(shortName="lnsp", **selection)
|
|
469
|
+
for var in self.variables:
|
|
470
|
+
LOG.debug(
|
|
471
|
+
f"Converting {var.short_name} at {t}"
|
|
472
|
+
+ (f" (ensemble member {ens})" if ens else "")
|
|
473
|
+
)
|
|
474
|
+
f_ml = fs_ml.select(shortName=var.short_name, **selection)
|
|
475
|
+
f_pl = mv.mvl_ml2hPa(lnsp, f_ml, self.pressure_levels)
|
|
476
|
+
fs_pl = mv.merge(fs_pl, f_pl)
|
|
477
|
+
|
|
478
|
+
# Create, validate, and cache dataset
|
|
479
|
+
ds = fs_pl.to_dataset()
|
|
480
|
+
ds = ds.rename(isobaricInhPa="level").expand_dims("time")
|
|
481
|
+
ds.attrs["pycontrails_version"] = pycontrails.__version__
|
|
482
|
+
self.cache_dataset(ds)
|