pycontrails 0.53.0__cp313-cp313-manylinux_2_17_x86_64.manylinux2014_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.

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