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,73 @@
1
+ """ECMWF Data Access."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pycontrails.datalib.ecmwf.arco_era5 import (
6
+ ARCOERA5,
7
+ open_arco_era5_model_level_data,
8
+ open_arco_era5_single_level,
9
+ )
10
+ from pycontrails.datalib.ecmwf.common import CDSCredentialsNotFound
11
+ from pycontrails.datalib.ecmwf.era5 import ERA5
12
+ from pycontrails.datalib.ecmwf.era5_model_level import ERA5ModelLevel
13
+ from pycontrails.datalib.ecmwf.hres import HRES
14
+ from pycontrails.datalib.ecmwf.hres_model_level import HRESModelLevel
15
+ from pycontrails.datalib.ecmwf.ifs import IFS
16
+ from pycontrails.datalib.ecmwf.model_levels import (
17
+ MODEL_LEVELS_PATH,
18
+ ml_to_pl,
19
+ model_level_pressure,
20
+ model_level_reference_pressure,
21
+ )
22
+ from pycontrails.datalib.ecmwf.variables import (
23
+ ECMWF_VARIABLES,
24
+ MODEL_LEVEL_VARIABLES,
25
+ PRESSURE_LEVEL_VARIABLES,
26
+ SURFACE_VARIABLES,
27
+ CloudAreaFraction,
28
+ CloudAreaFractionInLayer,
29
+ Divergence,
30
+ OzoneMassMixingRatio,
31
+ PotentialVorticity,
32
+ RelativeHumidity,
33
+ RelativeVorticity,
34
+ SpecificCloudIceWaterContent,
35
+ SpecificCloudLiquidWaterContent,
36
+ SurfaceSolarDownwardRadiation,
37
+ TOAIncidentSolarRadiation,
38
+ TopNetSolarRadiation,
39
+ TopNetThermalRadiation,
40
+ )
41
+
42
+ __all__ = [
43
+ "ARCOERA5",
44
+ "CDSCredentialsNotFound",
45
+ "ERA5",
46
+ "ERA5ModelLevel",
47
+ "HRES",
48
+ "HRESModelLevel",
49
+ "IFS",
50
+ "model_level_reference_pressure",
51
+ "model_level_pressure",
52
+ "ml_to_pl",
53
+ "open_arco_era5_model_level_data",
54
+ "open_arco_era5_single_level",
55
+ "CloudAreaFraction",
56
+ "CloudAreaFractionInLayer",
57
+ "Divergence",
58
+ "OzoneMassMixingRatio",
59
+ "PotentialVorticity",
60
+ "RelativeHumidity",
61
+ "RelativeVorticity",
62
+ "SpecificCloudIceWaterContent",
63
+ "SpecificCloudLiquidWaterContent",
64
+ "SurfaceSolarDownwardRadiation",
65
+ "TOAIncidentSolarRadiation",
66
+ "TopNetSolarRadiation",
67
+ "TopNetThermalRadiation",
68
+ "ECMWF_VARIABLES",
69
+ "MODEL_LEVELS_PATH",
70
+ "MODEL_LEVEL_VARIABLES",
71
+ "PRESSURE_LEVEL_VARIABLES",
72
+ "SURFACE_VARIABLES",
73
+ ]
@@ -0,0 +1,340 @@
1
+ """Support for `ARCO ERA5 <https://cloud.google.com/storage/docs/public-datasets/era5>`_.
2
+
3
+ This module supports:
4
+
5
+ - Downloading ARCO ERA5 model level data for specific times and pressure level variables.
6
+ - Downloading ARCO ERA5 single level data for specific times and single level variables.
7
+ - Interpolating model level data to a target lat-lon grid and pressure levels.
8
+ - Local caching of the downloaded and interpolated data as netCDF files.
9
+ - Opening cached data as a :class:`pycontrails.MetDataset` object.
10
+
11
+ This module requires the following additional dependencies:
12
+
13
+ - `gcsfs <https://gcsfs.readthedocs.io/en/latest/>`_
14
+ - `zarr <https://zarr.readthedocs.io/en/stable/>`_
15
+
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import datetime
21
+ import hashlib
22
+ from typing import Any
23
+
24
+ import numpy.typing as npt
25
+ import xarray as xr
26
+ from overrides import overrides
27
+
28
+ from pycontrails.core import cache, met_var
29
+ from pycontrails.core.met import MetDataset
30
+ from pycontrails.datalib._met_utils import metsource
31
+ from pycontrails.datalib.ecmwf import common as ecmwf_common
32
+ from pycontrails.datalib.ecmwf import model_levels as mlmod
33
+ from pycontrails.datalib.ecmwf import variables as ecmwf_variables
34
+
35
+ MODEL_LEVEL_STORE = "gs://gcp-public-data-arco-era5/ar/model-level-1h-0p25deg.zarr-v1"
36
+ # This combined store holds both pressure level and surface data
37
+ # It contains 273 variables (as of Sept 2024)
38
+ COMBINED_STORE = "gs://gcp-public-data-arco-era5/ar/full_37-1h-0p25deg-chunk-1.zarr-v3"
39
+
40
+
41
+ PRESSURE_LEVEL_VARIABLES = [
42
+ ecmwf_variables.Divergence,
43
+ ecmwf_variables.CloudAreaFractionInLayer,
44
+ met_var.Geopotential,
45
+ ecmwf_variables.OzoneMassMixingRatio,
46
+ ecmwf_variables.SpecificCloudIceWaterContent,
47
+ ecmwf_variables.SpecificCloudLiquidWaterContent,
48
+ met_var.SpecificHumidity,
49
+ # "specific_rain_water_content",
50
+ # "specific_snow_water_content",
51
+ met_var.AirTemperature,
52
+ met_var.EastwardWind,
53
+ met_var.NorthwardWind,
54
+ met_var.VerticalVelocity,
55
+ ecmwf_variables.RelativeVorticity,
56
+ ]
57
+
58
+
59
+ _met_vars_to_arco_model_level_mapping = {
60
+ ecmwf_variables.Divergence: "divergence",
61
+ ecmwf_variables.CloudAreaFractionInLayer: "fraction_of_cloud_cover",
62
+ met_var.Geopotential: "geopotential",
63
+ ecmwf_variables.OzoneMassMixingRatio: "ozone_mass_mixing_ratio",
64
+ ecmwf_variables.SpecificCloudIceWaterContent: "specific_cloud_ice_water_content",
65
+ ecmwf_variables.SpecificCloudLiquidWaterContent: "specific_cloud_liquid_water_content",
66
+ met_var.SpecificHumidity: "specific_humidity",
67
+ met_var.AirTemperature: "temperature",
68
+ met_var.EastwardWind: "u_component_of_wind",
69
+ met_var.NorthwardWind: "v_component_of_wind",
70
+ met_var.VerticalVelocity: "vertical_velocity",
71
+ ecmwf_variables.RelativeVorticity: "vorticity",
72
+ }
73
+
74
+ _met_vars_to_arco_surface_level_mapping = {
75
+ met_var.SurfacePressure: "surface_pressure",
76
+ ecmwf_variables.TOAIncidentSolarRadiation: "toa_incident_solar_radiation",
77
+ ecmwf_variables.TopNetSolarRadiation: "top_net_solar_radiation",
78
+ ecmwf_variables.TopNetThermalRadiation: "top_net_thermal_radiation",
79
+ ecmwf_variables.CloudAreaFraction: "total_cloud_cover",
80
+ ecmwf_variables.SurfaceSolarDownwardRadiation: "surface_solar_radiation_downwards",
81
+ }
82
+
83
+
84
+ def _open_arco_model_level_stores(
85
+ times: list[datetime.datetime],
86
+ variables: list[met_var.MetVariable],
87
+ ) -> tuple[xr.Dataset, xr.DataArray]:
88
+ """Open slices of the ARCO ERA5 model level Zarr stores."""
89
+ kw: dict[str, Any] = {"chunks": None, "consolidated": True} # keep type hint for mypy
90
+
91
+ # This is too slow to open with chunks={} or chunks="auto"
92
+ ds = xr.open_zarr(MODEL_LEVEL_STORE, **kw)
93
+ names = {
94
+ name: var.short_name
95
+ for var in variables
96
+ if (name := _met_vars_to_arco_model_level_mapping.get(var))
97
+ }
98
+ if not names:
99
+ msg = "No valid variables provided"
100
+ raise ValueError(msg)
101
+
102
+ ds = ds[list(names)].sel(time=times).rename(hybrid="model_level").rename_vars(names)
103
+ sp = xr.open_zarr(COMBINED_STORE, **kw)["surface_pressure"].sel(time=times)
104
+
105
+ # Chunk here in a way that is harmonious with the zarr store itself
106
+ # https://github.com/google-research/arco-era5?tab=readme-ov-file#025-model-level-data
107
+ ds = ds.chunk(time=1)
108
+ sp = sp.chunk(time=1)
109
+
110
+ return ds, sp
111
+
112
+
113
+ def open_arco_era5_model_level_data(
114
+ times: list[datetime.datetime],
115
+ variables: list[met_var.MetVariable],
116
+ pressure_levels: npt.ArrayLike,
117
+ ) -> xr.Dataset:
118
+ r"""Open ARCO ERA5 model level data for a specific time and variables.
119
+
120
+ Data is not loaded into memory, and the data is not cached.
121
+
122
+ Parameters
123
+ ----------
124
+ times : list[datetime.datetime]
125
+ Time of the data to open.
126
+ variables : list[met_var.MetVariable]
127
+ List of variables to open. Unsupported variables are ignored.
128
+ pressure_levels : npt.ArrayLike
129
+ Target pressure levels, [:math:`hPa`].
130
+
131
+ Returns
132
+ -------
133
+ xr.Dataset
134
+ Dataset with the requested variables on the target grid and pressure levels.
135
+ Data is reformatted for :class:`MetDataset` conventions.
136
+
137
+ References
138
+ ----------
139
+ - :cite:`carverARCOERA5AnalysisReadyCloudOptimized2023`
140
+ - `ARCO ERA5 moisture workflow <https://github.com/google-research/arco-era5/blob/main/docs/moisture_dataset.py>`_
141
+ - `Model Level Walkthrough <https://github.com/google-research/arco-era5/blob/main/docs/1-Model-Levels-Walkthrough.ipynb>`_
142
+ - `Surface Reanalysis Walkthrough <https://github.com/google-research/arco-era5/blob/main/docs/0-Surface-Reanalysis-Walkthrough.ipynb>`_
143
+ """
144
+ ds, sp = _open_arco_model_level_stores(times, variables)
145
+ out = mlmod.ml_to_pl(ds, pressure_levels, sp=sp)
146
+ return MetDataset(out).data
147
+
148
+
149
+ def open_arco_era5_single_level(
150
+ times: list[datetime.datetime],
151
+ variables: list[met_var.MetVariable],
152
+ ) -> xr.Dataset:
153
+ """Open ARCO ERA5 single level data for a specific date and variables.
154
+
155
+ Data is not loaded into memory, and the data is not cached.
156
+
157
+ Parameters
158
+ ----------
159
+ times : list[datetime.date]
160
+ Time of the data to open.
161
+ variables : list[met_var.MetVariable]
162
+ List of variables to open.
163
+
164
+ Returns
165
+ -------
166
+ xr.Dataset
167
+ Dataset with the requested variables.
168
+ Data is reformatted for :class:`MetDataset` conventions.
169
+
170
+ Raises
171
+ ------
172
+ FileNotFoundError
173
+ If the variable is not found at the requested date. This could
174
+ indicate that the variable is not available in the ARCO ERA5 dataset,
175
+ or that the time requested is outside the available range.
176
+ """
177
+ # This is too slow to open with chunks={} or chunks="auto"
178
+ ds = xr.open_zarr(COMBINED_STORE, consolidated=True, chunks=None)
179
+ names = {
180
+ name: var.short_name
181
+ for var in variables
182
+ if (name := _met_vars_to_arco_surface_level_mapping.get(var))
183
+ }
184
+ if not names:
185
+ msg = "No valid variables provided"
186
+ raise ValueError(msg)
187
+
188
+ ds = ds[list(names)].sel(time=times).rename_vars(names)
189
+
190
+ # But we need to chunk it here for lazy loading (the call expand_dims below
191
+ # would materialize the data if chunks=None). So we chunk in a way that is
192
+ # harmonious with the zarr store itself.
193
+ # https://github.com/google-research/arco-era5?tab=readme-ov-file#025-pressure-and-surface-level-data
194
+ ds = ds.chunk(time=1)
195
+
196
+ ds = ds.expand_dims(level=[-1])
197
+ return MetDataset(ds).data
198
+
199
+
200
+ class ARCOERA5(ecmwf_common.ECMWFAPI):
201
+ r"""ARCO ERA5 data accessed remotely through Google Cloud Storage.
202
+
203
+ This is a high-level interface to access and cache
204
+ `ARCO ERA5 <https://cloud.google.com/storage/docs/public-datasets/era5>`_
205
+ for a predefined set of times, variables, and pressure levels.
206
+
207
+ .. versionadded:: 0.50.0
208
+
209
+ Parameters
210
+ ----------
211
+ time : TimeInput
212
+ Time of the data to open.
213
+ variables : VariableInput
214
+ List of variables to open.
215
+ pressure_levels : PressureLevelInput, optional
216
+ Target pressure levels, [:math:`hPa`]. For pressure level data, this should be
217
+ a sorted (increasing or decreasing) list of integers. For single level data,
218
+ this should be ``-1``. By default, the pressure levels are set to the
219
+ pressure levels at each model level between 20,000 and 50,000 ft assuming a
220
+ constant surface pressure.
221
+ cachestore : CacheStore, optional
222
+ Cache store to use. By default, a new disk cache store is used. If None, no caching is done.
223
+ In this case, the data returned by :meth:`open_metdataset` is not loaded into memory.
224
+
225
+ References
226
+ ----------
227
+ :cite:`carverARCOERA5AnalysisReadyCloudOptimized2023`
228
+
229
+ See Also
230
+ --------
231
+ :func:`open_arco_era5_model_level_data`
232
+ :func:`open_arco_era5_single_level`
233
+ """
234
+
235
+ __marker = object()
236
+
237
+ def __init__(
238
+ self,
239
+ time: metsource.TimeInput,
240
+ variables: metsource.VariableInput,
241
+ pressure_levels: metsource.PressureLevelInput | None = None,
242
+ cachestore: cache.CacheStore | None = __marker, # type: ignore[assignment]
243
+ ) -> None:
244
+ self.timesteps = metsource.parse_timesteps(time)
245
+
246
+ if pressure_levels is None:
247
+ self.pressure_levels = mlmod.model_level_reference_pressure(20_000.0, 50_000.0)
248
+ else:
249
+ self.pressure_levels = metsource.parse_pressure_levels(pressure_levels)
250
+
251
+ self.paths = None
252
+ self.variables = metsource.parse_variables(variables, self.supported_variables)
253
+ self.cachestore = cache.DiskCacheStore() if cachestore is self.__marker else cachestore
254
+
255
+ @property
256
+ def pressure_level_variables(self) -> list[met_var.MetVariable]:
257
+ """Variables available in the ARCO ERA5 model level data.
258
+
259
+ Returns
260
+ -------
261
+ list[MetVariable] | None
262
+ List of MetVariable available in datasource
263
+ """
264
+ return PRESSURE_LEVEL_VARIABLES
265
+
266
+ @property
267
+ def single_level_variables(self) -> list[met_var.MetVariable]:
268
+ """Variables available in the ARCO ERA5 single level data.
269
+
270
+ Returns
271
+ -------
272
+ list[MetVariable] | None
273
+ List of MetVariable available in datasource
274
+ """
275
+ return ecmwf_variables.SURFACE_VARIABLES
276
+
277
+ @overrides
278
+ def download_dataset(self, times: list[datetime.datetime]) -> None:
279
+ if not times:
280
+ return
281
+
282
+ if self.is_single_level:
283
+ ds = open_arco_era5_single_level(times, self.variables)
284
+ else:
285
+ ds = open_arco_era5_model_level_data(times, self.variables, self.pressure_levels)
286
+
287
+ self.cache_dataset(ds)
288
+
289
+ @overrides
290
+ def create_cachepath(self, t: datetime.datetime) -> str:
291
+ if self.cachestore is None:
292
+ msg = "Attribute self.cachestore must be defined to create cache path"
293
+ raise ValueError(msg)
294
+
295
+ string = (
296
+ f"{t:%Y%m%d%H}-"
297
+ f"{'.'.join(str(p) for p in self.pressure_levels)}-"
298
+ f"{'.'.join(sorted(self.variable_shortnames))}-"
299
+ )
300
+ name = hashlib.md5(string.encode()).hexdigest()
301
+ cache_path = f"arcoera5-{name}.nc"
302
+
303
+ return self.cachestore.path(cache_path)
304
+
305
+ @overrides
306
+ def open_metdataset(
307
+ self,
308
+ dataset: xr.Dataset | None = None,
309
+ xr_kwargs: dict[str, Any] | None = None,
310
+ **kwargs: Any,
311
+ ) -> MetDataset:
312
+ if dataset:
313
+ msg = "Parameter 'dataset' is not supported for ARCO ERA5"
314
+ raise ValueError(msg)
315
+
316
+ if self.cachestore is None:
317
+ if self.is_single_level:
318
+ ds = open_arco_era5_single_level(self.timesteps, self.variables)
319
+ else:
320
+ ds = open_arco_era5_model_level_data(
321
+ self.timesteps, self.variables, self.pressure_levels
322
+ )
323
+ else:
324
+ xr_kwargs = xr_kwargs or {}
325
+ self.download(**xr_kwargs)
326
+
327
+ disk_cachepaths = [self.cachestore.get(f) for f in self._cachepaths]
328
+ ds = self.open_dataset(disk_cachepaths, **xr_kwargs)
329
+
330
+ mds = self._process_dataset(ds, **kwargs)
331
+ self.set_metadata(mds)
332
+ return mds
333
+
334
+ @overrides
335
+ def set_metadata(self, ds: xr.Dataset | MetDataset) -> None:
336
+ ds.attrs.update(
337
+ provider="ECMWF",
338
+ dataset="ERA5",
339
+ product="reanalysis",
340
+ )
@@ -0,0 +1,109 @@
1
+ """Common utilities for ECMWF Data Access."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from typing import Any
8
+
9
+ LOG = logging.getLogger(__name__)
10
+
11
+ import numpy as np
12
+ import pandas as pd
13
+ import xarray as xr
14
+ from overrides import overrides
15
+
16
+ from pycontrails.core import met
17
+ from pycontrails.datalib._met_utils import metsource
18
+
19
+
20
+ class ECMWFAPI(metsource.MetDataSource):
21
+ """Abstract class for all ECMWF data accessed remotely through CDS / MARS."""
22
+
23
+ @property
24
+ def variable_ecmwfids(self) -> list[int]:
25
+ """Return a list of variable ecmwf_ids.
26
+
27
+ Returns
28
+ -------
29
+ list[int]
30
+ List of int ECMWF param ids.
31
+ """
32
+ return [v.ecmwf_id for v in self.variables if v.ecmwf_id is not None]
33
+
34
+ def _process_dataset(self, ds: xr.Dataset, **kwargs: Any) -> met.MetDataset:
35
+ """Process the :class:`xr.Dataset` opened from cache or local files.
36
+
37
+ Parameters
38
+ ----------
39
+ ds : xr.Dataset
40
+ Dataset loaded from netcdf cache files or input paths.
41
+ **kwargs : Any
42
+ Keyword arguments passed through directly into :class:`MetDataset` constructor.
43
+
44
+ Returns
45
+ -------
46
+ MetDataset
47
+ """
48
+
49
+ # downselect variables
50
+ try:
51
+ ds = ds[self.variable_shortnames]
52
+ except KeyError as exc:
53
+ missing = set(self.variable_shortnames).difference(ds.variables)
54
+ msg = f"Input dataset is missing variables {missing}"
55
+ raise KeyError(msg) from exc
56
+
57
+ # downselect times
58
+ if not self.timesteps:
59
+ self.timesteps = ds["time"].values.astype("datetime64[ns]").tolist()
60
+ else:
61
+ try:
62
+ ds = ds.sel(time=self.timesteps)
63
+ except KeyError as exc:
64
+ # this snippet shows the missing times for convenience
65
+ np_timesteps = {np.datetime64(t, "ns") for t in self.timesteps}
66
+ missing_times = sorted(np_timesteps.difference(ds["time"].values)) # type: ignore[type-var]
67
+ msg = f"Input dataset is missing time coordinates {[str(t) for t in missing_times]}"
68
+ raise KeyError(msg) from exc
69
+
70
+ # downselect pressure level
71
+ # if "level" is not in dims and
72
+ # length of the requested pressure levels is 1
73
+ # expand the dims with this level
74
+ if "level" not in ds.dims and len(self.pressure_levels) == 1:
75
+ ds = ds.expand_dims(level=self.pressure_levels)
76
+
77
+ try:
78
+ ds = ds.sel(level=self.pressure_levels)
79
+ except KeyError as exc:
80
+ # this snippet shows the missing levels for convenience
81
+ missing_levels = sorted(set(self.pressure_levels) - set(ds["level"].values))
82
+ msg = f"Input dataset is missing level coordinates {missing_levels}"
83
+ raise KeyError(msg) from exc
84
+
85
+ # harmonize variable names
86
+ ds = met.standardize_variables(ds, self.variables)
87
+
88
+ kwargs.setdefault("cachestore", self.cachestore)
89
+ return met.MetDataset(ds, **kwargs)
90
+
91
+ @overrides
92
+ def cache_dataset(self, dataset: xr.Dataset) -> None:
93
+ if self.cachestore is None:
94
+ LOG.debug("Cache is turned off, skipping")
95
+ return
96
+
97
+ for t, ds_t in dataset.groupby("time", squeeze=False):
98
+ cache_path = self.create_cachepath(pd.Timestamp(t).to_pydatetime())
99
+ if os.path.exists(cache_path):
100
+ LOG.debug(f"Overwriting existing cache file {cache_path}")
101
+ # This may raise a PermissionError if the file is already open
102
+ # If this is the case, the user should explicitly close the file and try again
103
+ os.remove(cache_path)
104
+
105
+ ds_t.to_netcdf(cache_path)
106
+
107
+
108
+ class CDSCredentialsNotFound(Exception):
109
+ """Raise when CDS credentials are not found by :class:`cdsapi.Client` instance."""