pycontrails 0.58.0__cp314-cp314-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/__init__.py +70 -0
- pycontrails/_version.py +34 -0
- pycontrails/core/__init__.py +30 -0
- pycontrails/core/aircraft_performance.py +679 -0
- pycontrails/core/airports.py +228 -0
- pycontrails/core/cache.py +889 -0
- pycontrails/core/coordinates.py +174 -0
- pycontrails/core/fleet.py +483 -0
- pycontrails/core/flight.py +2185 -0
- pycontrails/core/flightplan.py +228 -0
- pycontrails/core/fuel.py +140 -0
- pycontrails/core/interpolation.py +702 -0
- pycontrails/core/met.py +2931 -0
- pycontrails/core/met_var.py +387 -0
- pycontrails/core/models.py +1321 -0
- pycontrails/core/polygon.py +549 -0
- pycontrails/core/rgi_cython.cpython-314-darwin.so +0 -0
- pycontrails/core/vector.py +2249 -0
- pycontrails/datalib/__init__.py +12 -0
- pycontrails/datalib/_met_utils/metsource.py +746 -0
- pycontrails/datalib/ecmwf/__init__.py +73 -0
- pycontrails/datalib/ecmwf/arco_era5.py +345 -0
- pycontrails/datalib/ecmwf/common.py +114 -0
- pycontrails/datalib/ecmwf/era5.py +554 -0
- pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
- pycontrails/datalib/ecmwf/hres.py +804 -0
- pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
- pycontrails/datalib/ecmwf/ifs.py +287 -0
- pycontrails/datalib/ecmwf/model_levels.py +435 -0
- pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
- pycontrails/datalib/ecmwf/variables.py +268 -0
- pycontrails/datalib/geo_utils.py +261 -0
- pycontrails/datalib/gfs/__init__.py +28 -0
- pycontrails/datalib/gfs/gfs.py +656 -0
- pycontrails/datalib/gfs/variables.py +104 -0
- pycontrails/datalib/goes.py +757 -0
- pycontrails/datalib/himawari/__init__.py +27 -0
- pycontrails/datalib/himawari/header_struct.py +266 -0
- pycontrails/datalib/himawari/himawari.py +667 -0
- pycontrails/datalib/landsat.py +589 -0
- pycontrails/datalib/leo_utils/__init__.py +5 -0
- pycontrails/datalib/leo_utils/correction.py +266 -0
- pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
- pycontrails/datalib/leo_utils/search.py +250 -0
- pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
- pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
- pycontrails/datalib/leo_utils/vis.py +59 -0
- pycontrails/datalib/sentinel.py +650 -0
- pycontrails/datalib/spire/__init__.py +5 -0
- pycontrails/datalib/spire/exceptions.py +62 -0
- pycontrails/datalib/spire/spire.py +604 -0
- pycontrails/ext/bada.py +42 -0
- pycontrails/ext/cirium.py +14 -0
- pycontrails/ext/empirical_grid.py +140 -0
- pycontrails/ext/synthetic_flight.py +431 -0
- pycontrails/models/__init__.py +1 -0
- pycontrails/models/accf.py +425 -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 +2742 -0
- pycontrails/models/cocip/cocip_params.py +305 -0
- pycontrails/models/cocip/cocip_uncertainty.py +291 -0
- pycontrails/models/cocip/contrail_properties.py +1530 -0
- pycontrails/models/cocip/output_formats.py +2270 -0
- pycontrails/models/cocip/radiative_forcing.py +1260 -0
- pycontrails/models/cocip/radiative_heating.py +520 -0
- pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -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 +2552 -0
- pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
- pycontrails/models/dry_advection.py +602 -0
- pycontrails/models/emissions/__init__.py +21 -0
- pycontrails/models/emissions/black_carbon.py +599 -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/extended_k15.py +1327 -0
- pycontrails/models/humidity_scaling/__init__.py +37 -0
- pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -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 +326 -0
- pycontrails/models/pcr.py +154 -0
- pycontrails/models/ps_model/__init__.py +18 -0
- pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
- pycontrails/models/ps_model/ps_grid.py +701 -0
- pycontrails/models/ps_model/ps_model.py +1000 -0
- pycontrails/models/ps_model/ps_operational_limits.py +525 -0
- pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
- pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
- pycontrails/models/sac.py +442 -0
- pycontrails/models/tau_cirrus.py +183 -0
- pycontrails/physics/__init__.py +1 -0
- pycontrails/physics/constants.py +117 -0
- pycontrails/physics/geo.py +1138 -0
- pycontrails/physics/jet.py +968 -0
- pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
- pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
- pycontrails/physics/thermo.py +551 -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 +187 -0
- pycontrails/utils/temp.py +50 -0
- pycontrails/utils/types.py +163 -0
- pycontrails-0.58.0.dist-info/METADATA +180 -0
- pycontrails-0.58.0.dist-info/RECORD +122 -0
- pycontrails-0.58.0.dist-info/WHEEL +6 -0
- pycontrails-0.58.0.dist-info/licenses/LICENSE +178 -0
- pycontrails-0.58.0.dist-info/licenses/NOTICE +43 -0
- pycontrails-0.58.0.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
"""ECWMF HRES forecast data access."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import logging
|
|
7
|
+
import pathlib
|
|
8
|
+
import sys
|
|
9
|
+
from contextlib import ExitStack
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
if sys.version_info >= (3, 12):
|
|
14
|
+
from typing import override
|
|
15
|
+
else:
|
|
16
|
+
from typing_extensions import override
|
|
17
|
+
|
|
18
|
+
LOG = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
import pandas as pd
|
|
21
|
+
import xarray as xr
|
|
22
|
+
|
|
23
|
+
import pycontrails
|
|
24
|
+
from pycontrails.core import cache
|
|
25
|
+
from pycontrails.core.met import MetDataset, MetVariable
|
|
26
|
+
from pycontrails.datalib._met_utils import metsource
|
|
27
|
+
from pycontrails.datalib.ecmwf.common import ECMWFAPI
|
|
28
|
+
from pycontrails.datalib.ecmwf.variables import PRESSURE_LEVEL_VARIABLES, SURFACE_VARIABLES
|
|
29
|
+
from pycontrails.utils import dependencies, iteration, temp
|
|
30
|
+
from pycontrails.utils.types import DatetimeLike
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from ecmwfapi import ECMWFService
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_forecast_filename(
|
|
37
|
+
forecast_time: datetime,
|
|
38
|
+
timestep: datetime,
|
|
39
|
+
cc: str = "A1",
|
|
40
|
+
S: str | None = None,
|
|
41
|
+
E: str = "1",
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Create forecast filename from ECMWF dissemination products.
|
|
44
|
+
|
|
45
|
+
See `ECMWF Dissemination <https://confluence.ecmwf.int/x/0EykDQ>`_ for more information::
|
|
46
|
+
|
|
47
|
+
The following dissemination filename convention is used for the
|
|
48
|
+
transmission of ECMWF dissemination products:
|
|
49
|
+
|
|
50
|
+
ccSMMDDHHIImmddhhiiE where:
|
|
51
|
+
|
|
52
|
+
cc is Dissemination stream name
|
|
53
|
+
S is dissemination data stream indicator
|
|
54
|
+
MMDDHHII is month, day, hour and minute on which the products are based
|
|
55
|
+
mmddhhii is month, day, hour and minute on which the products are valid at
|
|
56
|
+
ddhhii is set to “______” for Seasonal Forecasting System products
|
|
57
|
+
ii is set to 01 for high resolution forecast time step zero, type=fc, step=0
|
|
58
|
+
E is the Experiment Version Number' (as EXPVER in MARS, normally 1)
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
forecast_time : datetime
|
|
63
|
+
Forecast time to stage
|
|
64
|
+
timestep : datetime
|
|
65
|
+
Time within forecast
|
|
66
|
+
cc : str, optional
|
|
67
|
+
Dissemination stream name.
|
|
68
|
+
Defaults to "A1"
|
|
69
|
+
S : str, optional
|
|
70
|
+
Dissemination data stream indicator. If None, S is set to "S" for 6 or 18 hour forecast
|
|
71
|
+
and "D" for 0 or 12 hour forecast.
|
|
72
|
+
E : str, optional
|
|
73
|
+
Experiment Version Number.
|
|
74
|
+
Defaults to "1"
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
str
|
|
79
|
+
Filename to forecast file
|
|
80
|
+
|
|
81
|
+
Raises
|
|
82
|
+
------
|
|
83
|
+
ValueError
|
|
84
|
+
If ``forecast_time`` is not on a synoptic hour (00, 06, 12, 18)
|
|
85
|
+
If ``timestep`` is before ``forecast_time``
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
if forecast_time.hour % 6 != 0:
|
|
89
|
+
raise ValueError("Forecast time must have hour 0, 6, 12, or 18")
|
|
90
|
+
|
|
91
|
+
if timestep < forecast_time:
|
|
92
|
+
raise ValueError("Forecast timestep must be on or after forecast time")
|
|
93
|
+
|
|
94
|
+
if S is None:
|
|
95
|
+
S = "D" if forecast_time.hour in (0, 12) else "S"
|
|
96
|
+
|
|
97
|
+
forecast_time_str = forecast_time.strftime("%m%d%H%M")
|
|
98
|
+
timestep_str = timestep.strftime("%m%d%H")
|
|
99
|
+
|
|
100
|
+
# for some reason "ii" is set to 01 for the first forecast timestep
|
|
101
|
+
ii = "01" if forecast_time == timestep else "00"
|
|
102
|
+
|
|
103
|
+
return f"{cc}{S}{forecast_time_str}{timestep_str}{ii}{E}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class HRES(ECMWFAPI):
|
|
107
|
+
"""Class to support HRES data access, download, and organization.
|
|
108
|
+
|
|
109
|
+
Requires account with ECMWF and API key.
|
|
110
|
+
|
|
111
|
+
API credentials set in local ``~/.ecmwfapirc`` file:
|
|
112
|
+
|
|
113
|
+
.. code:: json
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
"url": "https://api.ecmwf.int/v1",
|
|
117
|
+
"email": "<email>",
|
|
118
|
+
"key": "<key>"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
Credentials can also be provided directly ``url`` ``key``, and ``email`` keyword args.
|
|
122
|
+
A third option is to set the environment variables ``ECMWF_API_URL``, ``ECMWF_API_KEY``,
|
|
123
|
+
and ``ECMWF_API_EMAIL``.
|
|
124
|
+
|
|
125
|
+
See `ecmwf-api-client <https://github.com/ecmwf/ecmwf-api-client>`_ documentation
|
|
126
|
+
for more information.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
time : metsource.TimeInput | None
|
|
131
|
+
The time range for data retrieval, either a single datetime or (start, end) datetime range.
|
|
132
|
+
Input must be a datetime-like or tuple of datetime-like
|
|
133
|
+
(datetime, :class:`pandas.Timestamp`, :class:`numpy.datetime64`)
|
|
134
|
+
specifying the (start, end) of the date range, inclusive.
|
|
135
|
+
If ``forecast_time`` is unspecified, the forecast reference time will
|
|
136
|
+
be assumed to be the nearest synoptic hour: 00, 06, 12, 18.
|
|
137
|
+
All subsequent times will be downloaded for relative to :attr:`forecast_time`.
|
|
138
|
+
If None, ``paths`` must be defined and all time coordinates will be loaded from files.
|
|
139
|
+
variables : metsource.VariableInput
|
|
140
|
+
Variable name (i.e. "air_temperature", ["air_temperature, relative_humidity"])
|
|
141
|
+
See :attr:`pressure_level_variables` for the list of available variables.
|
|
142
|
+
pressure_levels : metsource.PressureLevelInput, optional
|
|
143
|
+
Pressure levels for data, in hPa (mbar)
|
|
144
|
+
Set to -1 for to download surface level parameters.
|
|
145
|
+
Defaults to -1.
|
|
146
|
+
paths : str | list[str] | pathlib.Path | list[pathlib.Path] | None, optional
|
|
147
|
+
Path to CDS NetCDF files to load manually.
|
|
148
|
+
Can include glob patterns to load specific files.
|
|
149
|
+
Defaults to None, which looks for files in the :attr:`cachestore` or CDS.
|
|
150
|
+
grid : float, optional
|
|
151
|
+
Specify latitude/longitude grid spacing in data.
|
|
152
|
+
Defaults to 0.25.
|
|
153
|
+
stream : str, optional
|
|
154
|
+
- "oper" = high resolution forecast, atmospheric fields, run at hours 00Z and 12Z
|
|
155
|
+
- "scda" = short cut-off high resolution forecast, atmospheric fields,
|
|
156
|
+
run at hours 06Z and 18Z
|
|
157
|
+
- "enfo" = ensemble forecast, atmospheric fields, run at hours 00Z, 06Z, 12Z, and 18Z
|
|
158
|
+
|
|
159
|
+
Defaults to "oper" (HRES).
|
|
160
|
+
If the stream is incompatible with a provided forecast_time, a ``ValueError`` is raised.
|
|
161
|
+
See the `ECMWF documentation <https://confluence.ecmwf.int/display/DAC/ECMWF+open+data%3A+real-time+forecasts+from+IFS+and+AIFS>`_
|
|
162
|
+
for additional information.
|
|
163
|
+
field_type : str, optional
|
|
164
|
+
Field type can be e.g. forecast (fc), perturbed forecast (pf),
|
|
165
|
+
control forecast (cf), analysis (an).
|
|
166
|
+
Defaults to "fc".
|
|
167
|
+
forecast_time : DatetimeLike, optional
|
|
168
|
+
Specify forecast reference time (the time at which the forecast was initialized).
|
|
169
|
+
Defaults to None.
|
|
170
|
+
cachestore : cache.CacheStore | None, optional
|
|
171
|
+
Cache data store for staging data files.
|
|
172
|
+
Defaults to :class:`cache.DiskCacheStore`.
|
|
173
|
+
If None, cache is turned off.
|
|
174
|
+
url : str
|
|
175
|
+
Override `ecmwf-api-client <https://github.com/ecmwf/ecmwf-api-client>`_ url
|
|
176
|
+
key : str
|
|
177
|
+
Override `ecmwf-api-client <https://github.com/ecmwf/ecmwf-api-client>`_ key
|
|
178
|
+
email : str
|
|
179
|
+
Override `ecmwf-api-client <https://github.com/ecmwf/ecmwf-api-client>`_ email
|
|
180
|
+
|
|
181
|
+
Notes
|
|
182
|
+
-----
|
|
183
|
+
`MARS key word definitions <https://confluence.ecmwf.int/display/UDOC/Identification+keywords>`_
|
|
184
|
+
|
|
185
|
+
- `class <https://apps.ecmwf.int/codes/grib/format/mars/class/>`_:
|
|
186
|
+
in most cases this will be operational data, or "od"
|
|
187
|
+
- `stream <https://apps.ecmwf.int/codes/grib/format/mars/stream/>`_:
|
|
188
|
+
"enfo" = ensemble forecast, "oper" = atmospheric model/HRES
|
|
189
|
+
- `expver <https://confluence.ecmwf.int/pages/viewpage.action?pageId=124752178>`_:
|
|
190
|
+
experimental version, production data is 1 or 2
|
|
191
|
+
- `date <https://confluence.ecmwf.int/pages/viewpage.action?pageId=118817289>`_:
|
|
192
|
+
there are numerous acceptible date formats
|
|
193
|
+
- `time <https://confluence.ecmwf.int/pages/viewpage.action?pageId=118817378>`_:
|
|
194
|
+
forecast base time, always in synoptic time (0,6,12,18 UTC)
|
|
195
|
+
- `type <https://confluence.ecmwf.int/pages/viewpage.action?pageId=127315300>`_:
|
|
196
|
+
forecast (oper), perturbed or control forecast (enfo only), or analysis
|
|
197
|
+
- `levtype <https://confluence.ecmwf.int/pages/viewpage.action?pageId=149335319>`_:
|
|
198
|
+
options include surface, pressure levels, or model levels
|
|
199
|
+
- `levelist <https://confluence.ecmwf.int/pages/viewpage.action?pageId=149335403>`_:
|
|
200
|
+
list of levels in format specified by **levtype** `levelist`_
|
|
201
|
+
- `param <https://confluence.ecmwf.int/pages/viewpage.action?pageId=149335858>`_:
|
|
202
|
+
list of variables in catalog number, long name or short name
|
|
203
|
+
- `step <https://confluence.ecmwf.int/pages/viewpage.action?pageId=118820050>`_:
|
|
204
|
+
hourly time steps from base forecast time
|
|
205
|
+
- `number <https://confluence.ecmwf.int/pages/viewpage.action?pageId=149335478>`_:
|
|
206
|
+
for ensemble forecasts, ensemble numbers
|
|
207
|
+
- `format <https://confluence.ecmwf.int/pages/viewpage.action?pageId=116970058>`_:
|
|
208
|
+
specify netcdf instead of default grib, DEPRECATED `format`_
|
|
209
|
+
- `grid <https://confluence.ecmwf.int/pages/viewpage.action?pageId=123799065>`_:
|
|
210
|
+
specify model return grid spacing
|
|
211
|
+
|
|
212
|
+
Local ``paths`` are loaded using :func:`xarray.open_mfdataset`.
|
|
213
|
+
Pass ``xr_kwargs`` inputs to :meth:`open_metdataset` to customize file loading.
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
Examples
|
|
217
|
+
--------
|
|
218
|
+
>>> from datetime import datetime
|
|
219
|
+
>>> from pycontrails import GCPCacheStore
|
|
220
|
+
>>> from pycontrails.datalib.ecmwf import HRES
|
|
221
|
+
|
|
222
|
+
>>> # Store data files to local disk (default behavior)
|
|
223
|
+
>>> times = (datetime(2021, 5, 1, 2), datetime(2021, 5, 1, 3))
|
|
224
|
+
>>> hres = HRES(times, variables="air_temperature", pressure_levels=[300, 250])
|
|
225
|
+
|
|
226
|
+
>>> # Cache files to google cloud storage
|
|
227
|
+
>>> gcp_cache = GCPCacheStore(
|
|
228
|
+
... bucket="contrails-301217-unit-test",
|
|
229
|
+
... cache_dir="ecmwf",
|
|
230
|
+
... )
|
|
231
|
+
>>> hres = HRES(
|
|
232
|
+
... times,
|
|
233
|
+
... variables="air_temperature",
|
|
234
|
+
... pressure_levels=[300, 250],
|
|
235
|
+
... cachestore=gcp_cache
|
|
236
|
+
... )
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
__slots__ = ("email", "field_type", "forecast_time", "key", "server", "stream", "url")
|
|
240
|
+
|
|
241
|
+
#: stream type, "oper" or "scda" for atmospheric model/HRES, "enfo" for ensemble forecast.
|
|
242
|
+
stream: str
|
|
243
|
+
|
|
244
|
+
#: Field type, forecast ("fc"), perturbed forecast ("pf"),
|
|
245
|
+
#: control forecast ("cf"), analysis ("an").
|
|
246
|
+
field_type: str
|
|
247
|
+
|
|
248
|
+
#: Handle to ECMWFService client
|
|
249
|
+
server: ECMWFService
|
|
250
|
+
|
|
251
|
+
#: Forecast run time, either specified or assigned by the closest previous forecast run
|
|
252
|
+
forecast_time: datetime
|
|
253
|
+
|
|
254
|
+
__marker = object()
|
|
255
|
+
|
|
256
|
+
def __init__(
|
|
257
|
+
self,
|
|
258
|
+
time: metsource.TimeInput | None,
|
|
259
|
+
variables: metsource.VariableInput,
|
|
260
|
+
pressure_levels: metsource.PressureLevelInput = -1,
|
|
261
|
+
paths: str | list[str] | pathlib.Path | list[pathlib.Path] | None = None,
|
|
262
|
+
grid: float = 0.25,
|
|
263
|
+
stream: str = "oper",
|
|
264
|
+
field_type: str = "fc",
|
|
265
|
+
forecast_time: DatetimeLike | None = None,
|
|
266
|
+
cachestore: cache.CacheStore | None = __marker, # type: ignore[assignment]
|
|
267
|
+
url: str | None = None,
|
|
268
|
+
key: str | None = None,
|
|
269
|
+
email: str | None = None,
|
|
270
|
+
) -> None:
|
|
271
|
+
try:
|
|
272
|
+
from ecmwfapi import ECMWFService
|
|
273
|
+
except ModuleNotFoundError as e:
|
|
274
|
+
dependencies.raise_module_not_found_error(
|
|
275
|
+
name="HRES class",
|
|
276
|
+
package_name="ecmwf-api-client",
|
|
277
|
+
module_not_found_error=e,
|
|
278
|
+
pycontrails_optional_package="ecmwf",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# ERA5 now delays creating the server attribute until it is needed to download
|
|
282
|
+
# from CDS. We could do the same here.
|
|
283
|
+
self.server = ECMWFService("mars", url=url, key=key, email=email)
|
|
284
|
+
self.paths = paths
|
|
285
|
+
|
|
286
|
+
self.cachestore = cache.DiskCacheStore() if cachestore is self.__marker else cachestore
|
|
287
|
+
|
|
288
|
+
if time is None and paths is None:
|
|
289
|
+
raise ValueError("Time input is required when paths is None")
|
|
290
|
+
|
|
291
|
+
self.timesteps = metsource.parse_timesteps(time, freq="1h")
|
|
292
|
+
self.pressure_levels = metsource.parse_pressure_levels(
|
|
293
|
+
pressure_levels, self.supported_pressure_levels
|
|
294
|
+
)
|
|
295
|
+
self.variables = metsource.parse_variables(variables, self.supported_variables)
|
|
296
|
+
|
|
297
|
+
self.grid = metsource.parse_grid(grid, [0.1, 0.25, 0.5, 1]) # lat/lon degree resolution
|
|
298
|
+
|
|
299
|
+
# "fc" = forecast
|
|
300
|
+
# "pf" = perturbed forecast
|
|
301
|
+
# "cf" = control forecast
|
|
302
|
+
# "an" = analysis
|
|
303
|
+
if field_type not in ("fc", "pf", "cf", "an"):
|
|
304
|
+
msg = "Parameter field_type must be 'fc', 'pf', 'cf', or 'an'"
|
|
305
|
+
raise ValueError(msg)
|
|
306
|
+
|
|
307
|
+
self.field_type = field_type
|
|
308
|
+
|
|
309
|
+
# set specific forecast time is requested
|
|
310
|
+
if forecast_time is not None:
|
|
311
|
+
forecast_time_pd = pd.to_datetime(forecast_time)
|
|
312
|
+
if forecast_time_pd.hour % 6:
|
|
313
|
+
raise ValueError("Forecast hour must be on one of 00, 06, 12, 18")
|
|
314
|
+
|
|
315
|
+
self.forecast_time = metsource.round_hour(forecast_time_pd.to_pydatetime(), 6)
|
|
316
|
+
|
|
317
|
+
# if no specific forecast is requested, set the forecast time using timesteps
|
|
318
|
+
elif self.timesteps:
|
|
319
|
+
# round first element to the nearest 6 hour time (00, 06, 12, 18 UTC) for forecast_time
|
|
320
|
+
self.forecast_time = metsource.round_hour(self.timesteps[0], 6)
|
|
321
|
+
|
|
322
|
+
# NOTE: when no forecast_time or time input, forecast_time is defined in _open_and_cache
|
|
323
|
+
# This could occur when only the paths parameter is provided
|
|
324
|
+
|
|
325
|
+
# "enfo" = ensemble forecast
|
|
326
|
+
# "oper" = atmospheric model/HRES for 00 and 12 model runs
|
|
327
|
+
# "scda" = atmospheric model/HRES for 06 and 18 model runs
|
|
328
|
+
available_streams = ("oper", "enfo", "scda")
|
|
329
|
+
if stream not in available_streams:
|
|
330
|
+
msg = f"Parameter stream must be one of {available_streams}"
|
|
331
|
+
raise ValueError(msg)
|
|
332
|
+
|
|
333
|
+
if self.forecast_time.hour in (0, 12) and stream == "scda":
|
|
334
|
+
raise ValueError(
|
|
335
|
+
f"Stream {stream} is not compatible with forecast_time {self.forecast_time}. "
|
|
336
|
+
"Set stream='oper' for 00 and 12 UTC forecast times."
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
if self.forecast_time.hour in (6, 18) and stream == "oper":
|
|
340
|
+
raise ValueError(
|
|
341
|
+
f"Stream {stream} is not compatible with forecast_time {self.forecast_time}. "
|
|
342
|
+
"Set stream='scda' for 06 and 18 UTC forecast times."
|
|
343
|
+
)
|
|
344
|
+
self.stream = stream
|
|
345
|
+
|
|
346
|
+
def __repr__(self) -> str:
|
|
347
|
+
base = super().__repr__()
|
|
348
|
+
return (
|
|
349
|
+
f"{base}\n\tForecast time: {getattr(self, 'forecast_time', '')}\n\tSteps:"
|
|
350
|
+
f" {getattr(self, 'steps', '')}"
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
@staticmethod
|
|
354
|
+
def create_synoptic_time_ranges(
|
|
355
|
+
timesteps: list[pd.Timestamp],
|
|
356
|
+
) -> list[tuple[pd.Timestamp, pd.Timestamp]]:
|
|
357
|
+
"""Create synoptic time bounds encompassing date range.
|
|
358
|
+
|
|
359
|
+
Extracts time bounds for synoptic time range ([00:00, 11:59], [12:00, 23:59])
|
|
360
|
+
for a list of input timesteps.
|
|
361
|
+
|
|
362
|
+
Parameters
|
|
363
|
+
----------
|
|
364
|
+
timesteps : list[pd.Timestamp]
|
|
365
|
+
List of timesteps formatted as :class:`pd.Timestamps`.
|
|
366
|
+
Often this it the output from `pd.date_range()`
|
|
367
|
+
|
|
368
|
+
Returns
|
|
369
|
+
-------
|
|
370
|
+
list[tuple[pd.Timestamp, pd.Timestamp]]
|
|
371
|
+
List of tuple time bounds that can be used as inputs to :class:`HRES(time=...)`
|
|
372
|
+
"""
|
|
373
|
+
time_ranges = sorted({t.floor("12h") for t in timesteps})
|
|
374
|
+
|
|
375
|
+
if len(time_ranges) == 1:
|
|
376
|
+
return [(timesteps[0], timesteps[-1])]
|
|
377
|
+
|
|
378
|
+
time_ranges[0] = (timesteps[0], time_ranges[1] - pd.Timedelta(hours=1))
|
|
379
|
+
time_ranges[1:-1] = [(t, t + pd.Timedelta(hours=11)) for t in time_ranges[1:-1]]
|
|
380
|
+
time_ranges[-1] = (time_ranges[-1], timesteps[-1])
|
|
381
|
+
|
|
382
|
+
return time_ranges
|
|
383
|
+
|
|
384
|
+
@property
|
|
385
|
+
def hash(self) -> str:
|
|
386
|
+
"""Generate a unique hash for this datasource.
|
|
387
|
+
|
|
388
|
+
Returns
|
|
389
|
+
-------
|
|
390
|
+
str
|
|
391
|
+
Unique hash for met instance (sha1)
|
|
392
|
+
"""
|
|
393
|
+
hashstr = (
|
|
394
|
+
f"{self.__class__.__name__}{self.timesteps}{self.variable_shortnames}"
|
|
395
|
+
f"{self.pressure_levels}{self.grid}{self.forecast_time}{self.field_type}{self.stream}"
|
|
396
|
+
)
|
|
397
|
+
return hashlib.sha1(bytes(hashstr, "utf-8")).hexdigest()
|
|
398
|
+
|
|
399
|
+
@property
|
|
400
|
+
def pressure_level_variables(self) -> list[MetVariable]:
|
|
401
|
+
"""ECMWF pressure level parameters.
|
|
402
|
+
|
|
403
|
+
Returns
|
|
404
|
+
-------
|
|
405
|
+
list[MetVariable] | None
|
|
406
|
+
List of MetVariable available in datasource
|
|
407
|
+
"""
|
|
408
|
+
return PRESSURE_LEVEL_VARIABLES
|
|
409
|
+
|
|
410
|
+
@property
|
|
411
|
+
def single_level_variables(self) -> list[MetVariable]:
|
|
412
|
+
"""ECMWF surface level parameters.
|
|
413
|
+
|
|
414
|
+
Returns
|
|
415
|
+
-------
|
|
416
|
+
list[MetVariable] | None
|
|
417
|
+
List of MetVariable available in datasource
|
|
418
|
+
"""
|
|
419
|
+
return SURFACE_VARIABLES
|
|
420
|
+
|
|
421
|
+
@property
|
|
422
|
+
def supported_pressure_levels(self) -> list[int]:
|
|
423
|
+
"""Get pressure levels available from MARS.
|
|
424
|
+
|
|
425
|
+
Returns
|
|
426
|
+
-------
|
|
427
|
+
list[int]
|
|
428
|
+
List of integer pressure level values
|
|
429
|
+
"""
|
|
430
|
+
return [
|
|
431
|
+
1000,
|
|
432
|
+
950,
|
|
433
|
+
925,
|
|
434
|
+
900,
|
|
435
|
+
850,
|
|
436
|
+
800,
|
|
437
|
+
700,
|
|
438
|
+
600,
|
|
439
|
+
500,
|
|
440
|
+
400,
|
|
441
|
+
300,
|
|
442
|
+
250,
|
|
443
|
+
200,
|
|
444
|
+
150,
|
|
445
|
+
100,
|
|
446
|
+
70,
|
|
447
|
+
50,
|
|
448
|
+
30,
|
|
449
|
+
20,
|
|
450
|
+
10,
|
|
451
|
+
7,
|
|
452
|
+
5,
|
|
453
|
+
3,
|
|
454
|
+
2,
|
|
455
|
+
1,
|
|
456
|
+
-1,
|
|
457
|
+
]
|
|
458
|
+
|
|
459
|
+
@property
|
|
460
|
+
def step_offset(self) -> int:
|
|
461
|
+
"""Difference between :attr:`forecast_time` and first timestep.
|
|
462
|
+
|
|
463
|
+
Returns
|
|
464
|
+
-------
|
|
465
|
+
int
|
|
466
|
+
Number of steps to offset in order to retrieve data starting from input time.
|
|
467
|
+
Returns 0 if :attr:`timesteps` is empty when loading from :attr:`paths`.
|
|
468
|
+
"""
|
|
469
|
+
if self.timesteps:
|
|
470
|
+
return int((self.timesteps[0] - self.forecast_time).total_seconds() // 3600)
|
|
471
|
+
|
|
472
|
+
return 0
|
|
473
|
+
|
|
474
|
+
@property
|
|
475
|
+
def steps(self) -> list[int]:
|
|
476
|
+
"""Forecast steps from :attr:`forecast_time` corresponding within input :attr:`time`.
|
|
477
|
+
|
|
478
|
+
Returns
|
|
479
|
+
-------
|
|
480
|
+
list[int]
|
|
481
|
+
List of forecast steps relative to :attr:`forecast_time`
|
|
482
|
+
"""
|
|
483
|
+
return [self.step_offset + i for i in range(len(self.timesteps))]
|
|
484
|
+
|
|
485
|
+
def list_from_mars(self) -> str:
|
|
486
|
+
"""List metadata on query from MARS.
|
|
487
|
+
|
|
488
|
+
Returns
|
|
489
|
+
-------
|
|
490
|
+
str
|
|
491
|
+
Metadata for MARS request.
|
|
492
|
+
Note this is queued the same as data requests.
|
|
493
|
+
"""
|
|
494
|
+
request = self.generate_mars_request(self.forecast_time, self.steps, request_type="list")
|
|
495
|
+
|
|
496
|
+
# hold downloaded file in named temp file
|
|
497
|
+
with temp.temp_file() as mars_temp_filename:
|
|
498
|
+
LOG.debug("Performing MARS request: %s", request)
|
|
499
|
+
self.server.execute(request, target=mars_temp_filename)
|
|
500
|
+
|
|
501
|
+
with open(mars_temp_filename) as f:
|
|
502
|
+
return f.read()
|
|
503
|
+
|
|
504
|
+
def generate_mars_request(
|
|
505
|
+
self,
|
|
506
|
+
forecast_time: datetime | None = None,
|
|
507
|
+
steps: list[int] | None = None,
|
|
508
|
+
request_type: str = "retrieve",
|
|
509
|
+
request_format: str = "mars",
|
|
510
|
+
) -> str | dict[str, Any]:
|
|
511
|
+
"""Generate MARS request in MARS request syntax.
|
|
512
|
+
|
|
513
|
+
Parameters
|
|
514
|
+
----------
|
|
515
|
+
forecast_time : :class:`datetime`, optional
|
|
516
|
+
Base datetime for the forecast.
|
|
517
|
+
Defaults to :attr:`forecast_time`.
|
|
518
|
+
steps : list[int], optional
|
|
519
|
+
list of steps.
|
|
520
|
+
Defaults to :attr:`steps`.
|
|
521
|
+
request_type : str, optional
|
|
522
|
+
"retrieve" for download request or "list" for metadata request.
|
|
523
|
+
Defaults to "retrieve".
|
|
524
|
+
request_format : str, optional
|
|
525
|
+
"mars" for MARS string format, or "dict" for dict version.
|
|
526
|
+
Defaults to "mars".
|
|
527
|
+
|
|
528
|
+
Returns
|
|
529
|
+
-------
|
|
530
|
+
str | dict[str, Any]
|
|
531
|
+
Returns MARS query string if ``request_format`` is "mars".
|
|
532
|
+
Returns dict query if ``request_format`` is "dict"
|
|
533
|
+
|
|
534
|
+
Notes
|
|
535
|
+
-----
|
|
536
|
+
Brief overview of `MARS request syntax
|
|
537
|
+
<https://confluence.ecmwf.int/display/WEBAPI/Brief+MARS+request+syntax>`_
|
|
538
|
+
"""
|
|
539
|
+
|
|
540
|
+
if forecast_time is None:
|
|
541
|
+
forecast_time = self.forecast_time
|
|
542
|
+
|
|
543
|
+
if steps is None:
|
|
544
|
+
steps = self.steps
|
|
545
|
+
|
|
546
|
+
# set date/time for file
|
|
547
|
+
date = forecast_time.strftime("%Y%m%d")
|
|
548
|
+
time = forecast_time.strftime("%H")
|
|
549
|
+
|
|
550
|
+
# make request of mars
|
|
551
|
+
request: dict[str, Any] = {
|
|
552
|
+
"class": "od", # operational data
|
|
553
|
+
"stream": self.stream,
|
|
554
|
+
"expver": "1", # production data only
|
|
555
|
+
"date": date,
|
|
556
|
+
"time": time,
|
|
557
|
+
"type": self.field_type,
|
|
558
|
+
"param": f"{'/'.join(self.variable_shortnames)}",
|
|
559
|
+
"step": f"{'/'.join([str(s) for s in steps])}",
|
|
560
|
+
"grid": f"{self.grid}/{self.grid}",
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if self.pressure_levels != [-1]:
|
|
564
|
+
request["levtype"] = "pl"
|
|
565
|
+
request["levelist"] = f"{'/'.join([str(pl) for pl in self.pressure_levels])}"
|
|
566
|
+
else:
|
|
567
|
+
request["levtype"] = "sfc"
|
|
568
|
+
|
|
569
|
+
if request_format == "dict":
|
|
570
|
+
return request
|
|
571
|
+
|
|
572
|
+
levelist = f",\n\tlevelist={request['levelist']}" if self.pressure_levels != [-1] else ""
|
|
573
|
+
return (
|
|
574
|
+
f"{request_type},\n\tclass={request['class']},\n\tstream={request['stream']},"
|
|
575
|
+
f"\n\texpver={request['expver']},\n\tdate={request['date']},"
|
|
576
|
+
f"\n\ttime={request['time']},\n\ttype={request['type']},"
|
|
577
|
+
f"\n\tparam={request['param']},\n\tstep={request['step']},"
|
|
578
|
+
f"\n\tgrid={request['grid']},\n\tlevtype={request['levtype']}{levelist}"
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
@override
|
|
582
|
+
def create_cachepath(self, t: datetime) -> str:
|
|
583
|
+
if self.cachestore is None:
|
|
584
|
+
raise ValueError("self.cachestore attribute must be defined to create cache path")
|
|
585
|
+
|
|
586
|
+
# get forecast_time and step for specific file
|
|
587
|
+
datestr = self.forecast_time.strftime("%Y%m%d-%H")
|
|
588
|
+
|
|
589
|
+
# get step relative to forecast forecast_time
|
|
590
|
+
step = self.step_offset + self.timesteps.index(t)
|
|
591
|
+
|
|
592
|
+
# single level or pressure level
|
|
593
|
+
levtype = "sl" if self.pressure_levels == [-1] else "pl"
|
|
594
|
+
suffix = f"hres{levtype}{self.grid}{self.stream}{self.field_type}"
|
|
595
|
+
|
|
596
|
+
# return cache path
|
|
597
|
+
return self.cachestore.path(f"{datestr}-{step}-{suffix}.nc")
|
|
598
|
+
|
|
599
|
+
@override
|
|
600
|
+
def download_dataset(self, times: list[datetime]) -> None:
|
|
601
|
+
"""Download data from data source for input times.
|
|
602
|
+
|
|
603
|
+
Parameters
|
|
604
|
+
----------
|
|
605
|
+
times : list[:class:`datetime`]
|
|
606
|
+
List of datetimes to download and store in cache datastore
|
|
607
|
+
"""
|
|
608
|
+
|
|
609
|
+
# get step relative to forecast forecast_time
|
|
610
|
+
steps = [self.step_offset + self.timesteps.index(t) for t in times]
|
|
611
|
+
LOG.debug(f"Downloading HRES dataset for base time {self.forecast_time} and steps {steps}")
|
|
612
|
+
|
|
613
|
+
# download in sets of 24
|
|
614
|
+
if len(steps) > 24:
|
|
615
|
+
for _steps in iteration.chunk_list(steps, 24):
|
|
616
|
+
self._download_file(_steps)
|
|
617
|
+
elif len(steps) > 0:
|
|
618
|
+
self._download_file(steps)
|
|
619
|
+
|
|
620
|
+
@override
|
|
621
|
+
def open_metdataset(
|
|
622
|
+
self,
|
|
623
|
+
dataset: xr.Dataset | None = None,
|
|
624
|
+
xr_kwargs: dict[str, Any] | None = None,
|
|
625
|
+
**kwargs: Any,
|
|
626
|
+
) -> MetDataset:
|
|
627
|
+
xr_kwargs = xr_kwargs or {}
|
|
628
|
+
|
|
629
|
+
# short-circuit dataset or file paths if provided
|
|
630
|
+
if dataset is not None:
|
|
631
|
+
ds = self._preprocess_hres_dataset(dataset)
|
|
632
|
+
|
|
633
|
+
# load from local paths
|
|
634
|
+
elif self.paths is not None:
|
|
635
|
+
ds = self._open_and_cache(xr_kwargs)
|
|
636
|
+
|
|
637
|
+
# download from MARS
|
|
638
|
+
else:
|
|
639
|
+
if self.cachestore is None:
|
|
640
|
+
raise ValueError("Cachestore is required to download data")
|
|
641
|
+
|
|
642
|
+
# confirm files are downloaded from CDS or MARS
|
|
643
|
+
self.download(**xr_kwargs)
|
|
644
|
+
|
|
645
|
+
# ensure all files are guaranteed to be available locally here
|
|
646
|
+
# this would download a file from a remote (e.g. GCP) cache
|
|
647
|
+
disk_cachepaths = [self.cachestore.get(f) for f in self._cachepaths]
|
|
648
|
+
|
|
649
|
+
# open cache files as xr.Dataset
|
|
650
|
+
ds = self.open_dataset(disk_cachepaths, **xr_kwargs)
|
|
651
|
+
|
|
652
|
+
ds.attrs.setdefault("pycontrails_version", pycontrails.__version__)
|
|
653
|
+
|
|
654
|
+
# run the same ECMWF-specific processing on the dataset
|
|
655
|
+
mds = self._process_dataset(ds, **kwargs)
|
|
656
|
+
|
|
657
|
+
self.set_metadata(mds)
|
|
658
|
+
return mds
|
|
659
|
+
|
|
660
|
+
@override
|
|
661
|
+
def set_metadata(self, ds: xr.Dataset | MetDataset) -> None:
|
|
662
|
+
if self.stream in ("oper", "scda"):
|
|
663
|
+
product = "forecast"
|
|
664
|
+
elif self.stream == "enfo":
|
|
665
|
+
product = "ensemble"
|
|
666
|
+
else:
|
|
667
|
+
msg = f"Unknown stream type {self.stream}"
|
|
668
|
+
raise ValueError(msg)
|
|
669
|
+
|
|
670
|
+
ds.attrs.update(
|
|
671
|
+
provider="ECMWF",
|
|
672
|
+
dataset="HRES",
|
|
673
|
+
product=product,
|
|
674
|
+
radiation_accumulated=True,
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
def _open_and_cache(self, xr_kwargs: dict[str, Any]) -> xr.Dataset:
|
|
678
|
+
"""Open and cache :class:`xr.Dataset` from :attr:`self.paths`.
|
|
679
|
+
|
|
680
|
+
Parameters
|
|
681
|
+
----------
|
|
682
|
+
xr_kwargs : dict[str, Any]
|
|
683
|
+
Additional kwargs passed directly to :func:`xarray.open_mfdataset`.
|
|
684
|
+
See :meth:`open_metdataset`.
|
|
685
|
+
|
|
686
|
+
Returns
|
|
687
|
+
-------
|
|
688
|
+
xr.Dataset
|
|
689
|
+
Dataset opened from local paths.
|
|
690
|
+
"""
|
|
691
|
+
|
|
692
|
+
if self.paths is None:
|
|
693
|
+
raise ValueError("Attribute `self.paths` must be defined to open and cache")
|
|
694
|
+
|
|
695
|
+
# if timesteps are defined and all timesteps are cached already
|
|
696
|
+
# then we can skip loading
|
|
697
|
+
if self.timesteps and self.cachestore and not self.list_timesteps_not_cached(**xr_kwargs):
|
|
698
|
+
LOG.debug("All timesteps already in cache store")
|
|
699
|
+
disk_cachepaths = [self.cachestore.get(f) for f in self._cachepaths]
|
|
700
|
+
return self.open_dataset(disk_cachepaths, **xr_kwargs)
|
|
701
|
+
|
|
702
|
+
# set default parameters for loading grib files
|
|
703
|
+
xr_kwargs.setdefault("engine", "cfgrib")
|
|
704
|
+
xr_kwargs.setdefault("combine", "nested")
|
|
705
|
+
xr_kwargs.setdefault("concat_dim", "step")
|
|
706
|
+
xr_kwargs.setdefault("parallel", False)
|
|
707
|
+
ds = self.open_dataset(self.paths, **xr_kwargs)
|
|
708
|
+
|
|
709
|
+
# set forecast time if it's not defined (this occurs when only the paths param is provided)
|
|
710
|
+
if not hasattr(self, "forecast_time"):
|
|
711
|
+
self.forecast_time = ds["time"].values.astype("datetime64[s]").tolist()
|
|
712
|
+
|
|
713
|
+
# check that forecast_time is correct if defined
|
|
714
|
+
# note the "time" coordinate here is the HRES forecast_time
|
|
715
|
+
elif self.forecast_time != ds["time"].values.astype("datetime64[s]").tolist():
|
|
716
|
+
raise ValueError(
|
|
717
|
+
f"HRES.forecast_time {self.forecast_time} is not the same forecast time listed"
|
|
718
|
+
" in file"
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
ds = self._preprocess_hres_dataset(ds)
|
|
722
|
+
|
|
723
|
+
# set timesteps if not defined
|
|
724
|
+
# note that "time" is now the actual timestep coordinates
|
|
725
|
+
if not self.timesteps:
|
|
726
|
+
self.timesteps = ds["time"].values.astype("datetime64[s]").tolist()
|
|
727
|
+
|
|
728
|
+
self.cache_dataset(ds)
|
|
729
|
+
|
|
730
|
+
return ds
|
|
731
|
+
|
|
732
|
+
def _download_file(self, steps: list[int]) -> None:
|
|
733
|
+
"""Download data file for base datetime and timesteps.
|
|
734
|
+
|
|
735
|
+
Overwrites files if they already exists.
|
|
736
|
+
|
|
737
|
+
Parameters
|
|
738
|
+
----------
|
|
739
|
+
steps : list[int]
|
|
740
|
+
Steps to download relative to base date
|
|
741
|
+
"""
|
|
742
|
+
request = self.generate_mars_request(self.forecast_time, steps)
|
|
743
|
+
|
|
744
|
+
# Open ExitStack to control temp_file context manager
|
|
745
|
+
with ExitStack() as stack:
|
|
746
|
+
# hold downloaded file in named temp file
|
|
747
|
+
mars_temp_grib_filename = stack.enter_context(temp.temp_file())
|
|
748
|
+
|
|
749
|
+
# retrieve data from MARS
|
|
750
|
+
LOG.debug(f"Performing MARS request: {request}")
|
|
751
|
+
self.server.execute(request, mars_temp_grib_filename)
|
|
752
|
+
|
|
753
|
+
# translate into netcdf from grib
|
|
754
|
+
LOG.debug("Translating file into netcdf")
|
|
755
|
+
ds = stack.enter_context(xr.open_dataset(mars_temp_grib_filename, engine="cfgrib"))
|
|
756
|
+
|
|
757
|
+
# run preprocessing before cache
|
|
758
|
+
ds = self._preprocess_hres_dataset(ds)
|
|
759
|
+
|
|
760
|
+
self.cache_dataset(ds)
|
|
761
|
+
|
|
762
|
+
def _preprocess_hres_dataset(self, ds: xr.Dataset) -> xr.Dataset:
|
|
763
|
+
"""Process HRES data before caching.
|
|
764
|
+
|
|
765
|
+
Parameters
|
|
766
|
+
----------
|
|
767
|
+
ds : xr.Dataset
|
|
768
|
+
Loaded :class:`xr.Dataset`
|
|
769
|
+
|
|
770
|
+
Returns
|
|
771
|
+
-------
|
|
772
|
+
xr.Dataset
|
|
773
|
+
Processed :class:`xr.Dataset`
|
|
774
|
+
"""
|
|
775
|
+
|
|
776
|
+
if "pycontrails_version" in ds.attrs:
|
|
777
|
+
LOG.debug("Input dataset processed with pycontrails > 0.29")
|
|
778
|
+
return ds
|
|
779
|
+
|
|
780
|
+
# for pressure levels, need to rename "level" field
|
|
781
|
+
if self.pressure_levels != [-1]:
|
|
782
|
+
ds = ds.rename({"isobaricInhPa": "level"})
|
|
783
|
+
|
|
784
|
+
# for single level, and singular pressure levels, add the level dimension
|
|
785
|
+
if len(self.pressure_levels) == 1:
|
|
786
|
+
ds = ds.expand_dims({"level": self.pressure_levels})
|
|
787
|
+
|
|
788
|
+
# for single time, add the step dimension and assign time coords to step
|
|
789
|
+
if ds["step"].size == 1:
|
|
790
|
+
if "step" not in ds.dims:
|
|
791
|
+
ds = ds.expand_dims({"step": [ds["step"].values]})
|
|
792
|
+
|
|
793
|
+
ds = ds.assign_coords({"valid_time": ("step", [ds["valid_time"].values])})
|
|
794
|
+
|
|
795
|
+
# rename fields and swap time dimension for step
|
|
796
|
+
ds = ds.rename({"time": "forecast_time"})
|
|
797
|
+
ds = ds.rename({"valid_time": "time"})
|
|
798
|
+
ds = ds.swap_dims({"step": "time"})
|
|
799
|
+
|
|
800
|
+
# drop step/number
|
|
801
|
+
ds = ds.drop_vars(["step", "number"], errors="ignore")
|
|
802
|
+
|
|
803
|
+
ds.attrs["pycontrails_version"] = pycontrails.__version__
|
|
804
|
+
return ds
|