pycontrails 0.54.0__cp312-cp312-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.

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