pycontrails 0.59.0__cp314-cp314-macosx_10_15_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 +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 +2936 -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 +764 -0
- pycontrails/datalib/gruan.py +343 -0
- pycontrails/datalib/himawari/__init__.py +27 -0
- pycontrails/datalib/himawari/header_struct.py +266 -0
- pycontrails/datalib/himawari/himawari.py +671 -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.59.0.dist-info/METADATA +179 -0
- pycontrails-0.59.0.dist-info/RECORD +123 -0
- pycontrails-0.59.0.dist-info/WHEEL +6 -0
- pycontrails-0.59.0.dist-info/licenses/LICENSE +178 -0
- pycontrails-0.59.0.dist-info/licenses/NOTICE +43 -0
- pycontrails-0.59.0.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
"""Model-level HRES data access from the ECMWF operational archive.
|
|
2
|
+
|
|
3
|
+
This module supports
|
|
4
|
+
|
|
5
|
+
- Retrieving model-level HRES data by submitting MARS requests through the ECMWF API.
|
|
6
|
+
- Processing retrieved model-level 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
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import contextlib
|
|
14
|
+
import hashlib
|
|
15
|
+
import logging
|
|
16
|
+
import sys
|
|
17
|
+
import warnings
|
|
18
|
+
from datetime import datetime, timedelta
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
if sys.version_info >= (3, 12):
|
|
22
|
+
from typing import override
|
|
23
|
+
else:
|
|
24
|
+
from typing_extensions import override
|
|
25
|
+
|
|
26
|
+
LOG = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
import pandas as pd
|
|
29
|
+
import xarray as xr
|
|
30
|
+
|
|
31
|
+
import pycontrails
|
|
32
|
+
from pycontrails.core import cache
|
|
33
|
+
from pycontrails.core.met import MetDataset, MetVariable
|
|
34
|
+
from pycontrails.datalib._met_utils import metsource
|
|
35
|
+
from pycontrails.datalib.ecmwf import model_levels as mlmod
|
|
36
|
+
from pycontrails.datalib.ecmwf.common import ECMWFAPI
|
|
37
|
+
from pycontrails.datalib.ecmwf.variables import MODEL_LEVEL_VARIABLES
|
|
38
|
+
from pycontrails.utils import dependencies, temp
|
|
39
|
+
from pycontrails.utils.types import DatetimeLike
|
|
40
|
+
|
|
41
|
+
LAST_STEP_1H = 96 # latest forecast step with 1 hour frequency
|
|
42
|
+
LAST_STEP_3H = 144 # latest forecast step with 3 hour frequency
|
|
43
|
+
LAST_STEP_6H = 240 # latest forecast step with 6 hour frequency
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class HRESModelLevel(ECMWFAPI):
|
|
47
|
+
"""Class to support model-level HRES data access, download, and organization.
|
|
48
|
+
|
|
49
|
+
The interface is similar to :class:`pycontrails.datalib.ecmwf.HRES`,
|
|
50
|
+
which downloads pressure-level data with much lower vertical resolution and single-level data.
|
|
51
|
+
Note, however, that only a subset of the pressure-level data available through the operational
|
|
52
|
+
archive is available as model-level data. As a consequence, this interface only
|
|
53
|
+
supports access to nominal HRES forecasts (corresponding to ``stream = "oper"`` and
|
|
54
|
+
``field_type = "fc"`` in :class:`pycontrails.datalib.ecmwf.HRES`) initialized at 00z and 12z.
|
|
55
|
+
|
|
56
|
+
Requires account with ECMWF and API key.
|
|
57
|
+
|
|
58
|
+
API credentials can be set in local ``~/.ecmwfapirc`` file:
|
|
59
|
+
|
|
60
|
+
.. code:: json
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
"url": "https://api.ecmwf.int/v1",
|
|
64
|
+
"email": "<email>",
|
|
65
|
+
"key": "<key>"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Credentials can also be provided directly in ``url``, ``key``, and ``email`` keyword args.
|
|
69
|
+
A third option is to set the environment variables ``ECMWF_API_URL``, ``ECMWF_API_KEY``,
|
|
70
|
+
and ``ECMWF_API_EMAIL``.
|
|
71
|
+
|
|
72
|
+
See `ecmwf-api-client <https://github.com/ecmwf/ecmwf-api-client>`_ documentation
|
|
73
|
+
for more information.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
time : metsource.TimeInput
|
|
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
|
+
All times will be downloaded in a single NetCDF file, which
|
|
83
|
+
ensures that exactly one request is submitted per file on tape accessed.
|
|
84
|
+
If ``forecast_time`` is unspecified, the forecast time will
|
|
85
|
+
be assumed to be the nearest synoptic hour available in the operational archive (00 or 12).
|
|
86
|
+
All subsequent times will be downloaded for relative to :attr:`forecast_time`.
|
|
87
|
+
variables : metsource.VariableInput
|
|
88
|
+
Variable name (i.e. "t", "air_temperature", ["air_temperature, specific_humidity"])
|
|
89
|
+
pressure_levels : metsource.PressureLevelInput, optional
|
|
90
|
+
Pressure levels for data, in hPa (mbar).
|
|
91
|
+
To download surface-level parameters, use :class:`pycontrails.datalib.ecmwf.HRES`.
|
|
92
|
+
Defaults to pressure levels that match model levels at a nominal surface pressure.
|
|
93
|
+
timestep_freq : str, optional
|
|
94
|
+
Manually set the timestep interval within the bounds defined by :attr:`time`.
|
|
95
|
+
Supports any string that can be passed to ``pandas.date_range(freq=...)``.
|
|
96
|
+
By default, this is set to the highest frequency that can supported the requested
|
|
97
|
+
time range ("1h" out to 96 hours, "3h" out to 144 hours, and "6h" out to 240 hours)
|
|
98
|
+
grid : float, optional
|
|
99
|
+
Specify latitude/longitude grid spacing in data.
|
|
100
|
+
By default, this is set to 0.1.
|
|
101
|
+
forecast_time : DatetimeLike, optional
|
|
102
|
+
Specify forecast by initialization time.
|
|
103
|
+
By default, set to the most recent forecast that includes the requested time range.
|
|
104
|
+
levels : list[int], optional
|
|
105
|
+
Specify ECMWF model levels to include in MARS requests.
|
|
106
|
+
By default, this is set to include all model levels.
|
|
107
|
+
cachestore : CacheStore | None, optional
|
|
108
|
+
Cache data store for staging processed netCDF files.
|
|
109
|
+
Defaults to :class:`pycontrails.core.cache.DiskCacheStore`.
|
|
110
|
+
If None, cache is turned off.
|
|
111
|
+
cache_download: bool, optional
|
|
112
|
+
If True, cache downloaded NetCDF files rather than storing them in a temporary file.
|
|
113
|
+
By default, False.
|
|
114
|
+
url : str
|
|
115
|
+
Override `ecmwf-api-client <https://github.com/ecmwf/ecmwf-api-client>`_ url
|
|
116
|
+
key : str
|
|
117
|
+
Override `ecmwf-api-client <https://github.com/ecmwf/ecmwf-api-client>`_ key
|
|
118
|
+
email : str
|
|
119
|
+
Override `ecmwf-api-client <https://github.com/ecmwf/ecmwf-api-client>`_ email
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
__marker = object()
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
time: metsource.TimeInput,
|
|
127
|
+
variables: metsource.VariableInput,
|
|
128
|
+
pressure_levels: metsource.PressureLevelInput | None = None,
|
|
129
|
+
timestep_freq: str | None = None,
|
|
130
|
+
grid: float | None = None,
|
|
131
|
+
forecast_time: DatetimeLike | None = None,
|
|
132
|
+
model_levels: list[int] | None = None,
|
|
133
|
+
cachestore: cache.CacheStore = __marker, # type: ignore[assignment]
|
|
134
|
+
cache_download: bool = False,
|
|
135
|
+
url: str | None = None,
|
|
136
|
+
key: str | None = None,
|
|
137
|
+
email: str | None = None,
|
|
138
|
+
) -> None:
|
|
139
|
+
# Parse and set each parameter to the instance
|
|
140
|
+
|
|
141
|
+
self.cachestore = cache.DiskCacheStore() if cachestore is self.__marker else cachestore
|
|
142
|
+
self.cache_download = cache_download
|
|
143
|
+
|
|
144
|
+
self.paths = None
|
|
145
|
+
|
|
146
|
+
self.url = url
|
|
147
|
+
self.key = key
|
|
148
|
+
self.email = email
|
|
149
|
+
|
|
150
|
+
if grid is None:
|
|
151
|
+
grid = 0.1
|
|
152
|
+
else:
|
|
153
|
+
grid_min = 0.1
|
|
154
|
+
if grid < grid_min:
|
|
155
|
+
msg = (
|
|
156
|
+
f"The highest resolution available is {grid_min} degrees. "
|
|
157
|
+
f"Your downloaded data will have resolution {grid}, but it is a "
|
|
158
|
+
f"reinterpolation of the {grid_min} degree data. The same interpolation can be "
|
|
159
|
+
"achieved directly with xarray."
|
|
160
|
+
)
|
|
161
|
+
warnings.warn(msg)
|
|
162
|
+
self.grid = grid
|
|
163
|
+
|
|
164
|
+
if model_levels is None:
|
|
165
|
+
model_levels = list(range(1, 138))
|
|
166
|
+
elif min(model_levels) < 1 or max(model_levels) > 137:
|
|
167
|
+
msg = "Retrieval model_levels must be between 1 and 137, inclusive."
|
|
168
|
+
raise ValueError(msg)
|
|
169
|
+
self.model_levels = model_levels
|
|
170
|
+
|
|
171
|
+
forecast_hours = metsource.parse_timesteps(time, freq="1h")
|
|
172
|
+
if forecast_time is None:
|
|
173
|
+
self.forecast_time = metsource.round_hour(forecast_hours[0], 12)
|
|
174
|
+
else:
|
|
175
|
+
forecast_time_pd = pd.to_datetime(forecast_time)
|
|
176
|
+
if (hour := forecast_time_pd.hour) % 12:
|
|
177
|
+
msg = f"Forecast hour must be one of 00 or 12 but is {hour:02d}."
|
|
178
|
+
raise ValueError(msg)
|
|
179
|
+
self.forecast_time = metsource.round_hour(forecast_time_pd.to_pydatetime(), 12)
|
|
180
|
+
|
|
181
|
+
last_step = (forecast_hours[-1] - self.forecast_time) / timedelta(hours=1)
|
|
182
|
+
if last_step > LAST_STEP_6H:
|
|
183
|
+
msg = (
|
|
184
|
+
f"Requested times requires forecast steps out to {last_step}, "
|
|
185
|
+
f"which is beyond latest available step of {LAST_STEP_6H}"
|
|
186
|
+
)
|
|
187
|
+
raise ValueError(msg)
|
|
188
|
+
|
|
189
|
+
datasource_timestep_freq = (
|
|
190
|
+
"1h" if last_step <= LAST_STEP_1H else "3h" if last_step <= LAST_STEP_3H else "6h"
|
|
191
|
+
)
|
|
192
|
+
if timestep_freq is None:
|
|
193
|
+
timestep_freq = datasource_timestep_freq
|
|
194
|
+
if not metsource.validate_timestep_freq(timestep_freq, datasource_timestep_freq):
|
|
195
|
+
msg = (
|
|
196
|
+
f"Forecast out to step {last_step} "
|
|
197
|
+
f"has timestep frequency of {datasource_timestep_freq} "
|
|
198
|
+
f"and cannot support requested timestep frequency of {timestep_freq}."
|
|
199
|
+
)
|
|
200
|
+
raise ValueError(msg)
|
|
201
|
+
|
|
202
|
+
self.timesteps = metsource.parse_timesteps(time, freq=timestep_freq)
|
|
203
|
+
if self.step_offset < 0:
|
|
204
|
+
msg = f"Selected forecast time {self.forecast_time} is after first timestep."
|
|
205
|
+
raise ValueError(msg)
|
|
206
|
+
|
|
207
|
+
if pressure_levels is None:
|
|
208
|
+
pressure_levels = mlmod.model_level_reference_pressure(20_000.0, 50_000.0)
|
|
209
|
+
self.pressure_levels = metsource.parse_pressure_levels(pressure_levels)
|
|
210
|
+
self.variables = metsource.parse_variables(variables, self.pressure_level_variables)
|
|
211
|
+
|
|
212
|
+
def __repr__(self) -> str:
|
|
213
|
+
base = super().__repr__()
|
|
214
|
+
return "\n\t".join(
|
|
215
|
+
[
|
|
216
|
+
base,
|
|
217
|
+
f"Forecast time: {getattr(self, 'forecast_time', '')}",
|
|
218
|
+
f"Steps: {getattr(self, 'steps', '')}",
|
|
219
|
+
]
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def get_forecast_steps(self, times: list[datetime]) -> list[int]:
|
|
223
|
+
"""Convert list of times to list of forecast steps.
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
times : list[datetime]
|
|
228
|
+
Times to convert to forecast steps
|
|
229
|
+
|
|
230
|
+
Returns
|
|
231
|
+
-------
|
|
232
|
+
list[int]
|
|
233
|
+
Forecast step at each time
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
def time_to_step(time: datetime) -> int:
|
|
237
|
+
step = (time - self.forecast_time) / timedelta(hours=1)
|
|
238
|
+
if not step.is_integer():
|
|
239
|
+
msg = (
|
|
240
|
+
f"Time-to-step conversion returned fractional forecast step {step} "
|
|
241
|
+
f"for timestep {time.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
242
|
+
)
|
|
243
|
+
raise ValueError(msg)
|
|
244
|
+
return int(step)
|
|
245
|
+
|
|
246
|
+
return [time_to_step(t) for t in times]
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def step_offset(self) -> int:
|
|
250
|
+
"""Difference between :attr:`forecast_time` and first timestep.
|
|
251
|
+
|
|
252
|
+
Returns
|
|
253
|
+
-------
|
|
254
|
+
int
|
|
255
|
+
Number of steps to offset in order to retrieve data starting from input time.
|
|
256
|
+
"""
|
|
257
|
+
return self.get_forecast_steps([self.timesteps[0]])[0]
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def steps(self) -> list[int]:
|
|
261
|
+
"""Forecast steps from :attr:`forecast_time` corresponding within input :attr:`time`.
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
list[int]
|
|
266
|
+
List of forecast steps relative to :attr:`forecast_time`
|
|
267
|
+
"""
|
|
268
|
+
return self.get_forecast_steps(self.timesteps)
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def pressure_level_variables(self) -> list[MetVariable]:
|
|
272
|
+
"""ECMWF pressure level parameters available on model levels.
|
|
273
|
+
|
|
274
|
+
Returns
|
|
275
|
+
-------
|
|
276
|
+
list[MetVariable]
|
|
277
|
+
List of MetVariable available in datasource
|
|
278
|
+
"""
|
|
279
|
+
return MODEL_LEVEL_VARIABLES
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def single_level_variables(self) -> list[MetVariable]:
|
|
283
|
+
"""ECMWF single-level parameters available on model levels.
|
|
284
|
+
|
|
285
|
+
Returns
|
|
286
|
+
-------
|
|
287
|
+
list[MetVariable]
|
|
288
|
+
Always returns an empty list.
|
|
289
|
+
To access single-level variables, use :class:`pycontrails.datalib.ecmwf.HRES`.
|
|
290
|
+
"""
|
|
291
|
+
return []
|
|
292
|
+
|
|
293
|
+
@override
|
|
294
|
+
def create_cachepath(self, t: datetime | pd.Timestamp) -> str:
|
|
295
|
+
"""Return cachepath to local HRES data file based on datetime.
|
|
296
|
+
|
|
297
|
+
This uniquely defines a cached data file with class parameters.
|
|
298
|
+
|
|
299
|
+
Parameters
|
|
300
|
+
----------
|
|
301
|
+
t : datetime | pd.Timestamp
|
|
302
|
+
Datetime of datafile
|
|
303
|
+
|
|
304
|
+
Returns
|
|
305
|
+
-------
|
|
306
|
+
str
|
|
307
|
+
Path to local HRES data file
|
|
308
|
+
"""
|
|
309
|
+
if self.cachestore is None:
|
|
310
|
+
msg = "Cachestore is required to create cache path"
|
|
311
|
+
raise ValueError(msg)
|
|
312
|
+
|
|
313
|
+
string = (
|
|
314
|
+
f"{t:%Y%m%d%H}-"
|
|
315
|
+
f"{self.forecast_time:%Y%m%d%H}-"
|
|
316
|
+
f"{'.'.join(str(p) for p in self.pressure_levels)}-"
|
|
317
|
+
f"{'.'.join(sorted(self.variable_shortnames))}-"
|
|
318
|
+
f"{self.grid}"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
name = hashlib.md5(string.encode()).hexdigest()
|
|
322
|
+
cache_path = f"hresml-{name}.nc"
|
|
323
|
+
|
|
324
|
+
return self.cachestore.path(cache_path)
|
|
325
|
+
|
|
326
|
+
@override
|
|
327
|
+
def download_dataset(self, times: list[datetime]) -> None:
|
|
328
|
+
# will always submit a single MARS request since each forecast is a separate file on tape
|
|
329
|
+
LOG.debug(f"Retrieving ERA5 data for times {times} from forecast {self.forecast_time}")
|
|
330
|
+
self._download_convert_cache_handler(times)
|
|
331
|
+
|
|
332
|
+
@override
|
|
333
|
+
def open_metdataset(
|
|
334
|
+
self,
|
|
335
|
+
dataset: xr.Dataset | None = None,
|
|
336
|
+
xr_kwargs: dict[str, Any] | None = None,
|
|
337
|
+
**kwargs: Any,
|
|
338
|
+
) -> MetDataset:
|
|
339
|
+
if dataset:
|
|
340
|
+
msg = "Parameter 'dataset' is not supported for Model-level ERA5 data"
|
|
341
|
+
raise ValueError(msg)
|
|
342
|
+
|
|
343
|
+
if self.cachestore is None:
|
|
344
|
+
msg = "Cachestore is required to download data"
|
|
345
|
+
raise ValueError(msg)
|
|
346
|
+
|
|
347
|
+
xr_kwargs = xr_kwargs or {}
|
|
348
|
+
self.download(**xr_kwargs)
|
|
349
|
+
|
|
350
|
+
disk_cachepaths = [self.cachestore.get(f) for f in self._cachepaths]
|
|
351
|
+
ds = self.open_dataset(disk_cachepaths, **xr_kwargs)
|
|
352
|
+
|
|
353
|
+
mds = self._process_dataset(ds, **kwargs)
|
|
354
|
+
|
|
355
|
+
self.set_metadata(mds)
|
|
356
|
+
return mds
|
|
357
|
+
|
|
358
|
+
@override
|
|
359
|
+
def set_metadata(self, ds: xr.Dataset | MetDataset) -> None:
|
|
360
|
+
ds.attrs.update(
|
|
361
|
+
provider="ECMWF", dataset="HRES", product="forecast", radiation_accumulated=True
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
def mars_request(self, times: list[datetime]) -> str:
|
|
365
|
+
"""Generate MARS request for specific list of times.
|
|
366
|
+
|
|
367
|
+
Parameters
|
|
368
|
+
----------
|
|
369
|
+
times : list[datetime]
|
|
370
|
+
Times included in MARS request.
|
|
371
|
+
|
|
372
|
+
Returns
|
|
373
|
+
-------
|
|
374
|
+
str
|
|
375
|
+
MARS request for submission to ECMWF API.
|
|
376
|
+
"""
|
|
377
|
+
date = self.forecast_time.strftime("%Y-%m-%d")
|
|
378
|
+
time = self.forecast_time.strftime("%H:%M:%S")
|
|
379
|
+
steps = self.get_forecast_steps(times)
|
|
380
|
+
# param 152 = log surface pressure, needed for model level conversion
|
|
381
|
+
grib_params = {*self.variable_ecmwfids, 152}
|
|
382
|
+
return (
|
|
383
|
+
f"retrieve,\n"
|
|
384
|
+
f"class=od,\n"
|
|
385
|
+
f"date={date},\n"
|
|
386
|
+
f"expver=1,\n"
|
|
387
|
+
f"levelist={'/'.join(str(lev) for lev in sorted(self.model_levels))},\n"
|
|
388
|
+
f"levtype=ml,\n"
|
|
389
|
+
f"param={'/'.join(str(p) for p in sorted(grib_params))},\n"
|
|
390
|
+
f"step={'/'.join(str(s) for s in sorted(steps))},\n"
|
|
391
|
+
f"stream=oper,\n"
|
|
392
|
+
f"time={time},\n"
|
|
393
|
+
f"type=fc,\n"
|
|
394
|
+
f"grid={self.grid}/{self.grid},\n"
|
|
395
|
+
"format=netcdf"
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def _set_server(self) -> None:
|
|
399
|
+
"""Set the ecmwfapi.ECMWFService instance."""
|
|
400
|
+
try:
|
|
401
|
+
from ecmwfapi import ECMWFService
|
|
402
|
+
except ModuleNotFoundError as e:
|
|
403
|
+
dependencies.raise_module_not_found_error(
|
|
404
|
+
name="HRESModelLevel._set_server method",
|
|
405
|
+
package_name="ecmwf-api-client",
|
|
406
|
+
module_not_found_error=e,
|
|
407
|
+
pycontrails_optional_package="ecmwf",
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
self.server = ECMWFService("mars", url=self.url, key=self.key, email=self.email)
|
|
411
|
+
|
|
412
|
+
def _download_convert_cache_handler(
|
|
413
|
+
self,
|
|
414
|
+
times: list[datetime],
|
|
415
|
+
) -> None:
|
|
416
|
+
"""Download, convert, and cache HRES model level data.
|
|
417
|
+
|
|
418
|
+
This function builds a MARS request and retrieves a single NetCDF file.
|
|
419
|
+
The calling function should ensure that all times will be contained
|
|
420
|
+
in a single file on tape in the MARS archive.
|
|
421
|
+
|
|
422
|
+
Because MARS requests treat dates and times as separate dimensions,
|
|
423
|
+
retrieved data will include the Cartesian product of all unique
|
|
424
|
+
dates and times in the list of specified times.
|
|
425
|
+
|
|
426
|
+
After retrieval, this function processes the NetCDF file
|
|
427
|
+
to produce the dataset specified by class attributes.
|
|
428
|
+
|
|
429
|
+
Parameters
|
|
430
|
+
----------
|
|
431
|
+
times : list[datetime]
|
|
432
|
+
Times to download in a single MARS request.
|
|
433
|
+
|
|
434
|
+
"""
|
|
435
|
+
if self.cachestore is None:
|
|
436
|
+
msg = "Cachestore is required to download and cache data"
|
|
437
|
+
raise ValueError(msg)
|
|
438
|
+
|
|
439
|
+
request = self.mars_request(times)
|
|
440
|
+
|
|
441
|
+
stack = contextlib.ExitStack()
|
|
442
|
+
if not self.cache_download:
|
|
443
|
+
target = stack.enter_context(temp.temp_file())
|
|
444
|
+
else:
|
|
445
|
+
name = hashlib.md5(request.encode()).hexdigest()
|
|
446
|
+
target = self.cachestore.path(f"hresml-{name}.nc")
|
|
447
|
+
|
|
448
|
+
with stack:
|
|
449
|
+
if not self.cache_download or not self.cachestore.exists(target):
|
|
450
|
+
if not hasattr(self, "server"):
|
|
451
|
+
self._set_server()
|
|
452
|
+
self.server.execute(request, target)
|
|
453
|
+
|
|
454
|
+
LOG.debug("Opening model level data file")
|
|
455
|
+
|
|
456
|
+
# Use a chunking scheme harmonious with self.cache_dataset, which groups by time
|
|
457
|
+
# Because ds_ml is dask-backed, nothing gets computed until cache_dataset is called
|
|
458
|
+
ds_ml = xr.open_dataset(target).chunk(time=1)
|
|
459
|
+
|
|
460
|
+
ds_ml = ds_ml.rename(level="model_level")
|
|
461
|
+
lnsp = ds_ml["lnsp"].sel(model_level=1)
|
|
462
|
+
ds_ml = ds_ml.drop_vars("lnsp")
|
|
463
|
+
|
|
464
|
+
ds = mlmod.ml_to_pl(ds_ml, target_pl=self.pressure_levels, lnsp=lnsp)
|
|
465
|
+
ds.attrs["pycontrails_version"] = pycontrails.__version__
|
|
466
|
+
self.cache_dataset(ds)
|