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,284 @@
1
+ """ECWMF IFS forecast data access."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import pathlib
7
+ import warnings
8
+ from datetime import datetime
9
+ from typing import Any
10
+
11
+ LOG = logging.getLogger(__name__)
12
+
13
+ import numpy as np
14
+ import pandas as pd
15
+ import xarray as xr
16
+ from overrides import overrides
17
+
18
+ from pycontrails.core import met
19
+ from pycontrails.datalib._met_utils import metsource
20
+ from pycontrails.datalib.ecmwf.variables import ECMWF_VARIABLES
21
+ from pycontrails.physics import constants
22
+ from pycontrails.utils.types import DatetimeLike
23
+
24
+
25
+ class IFS(metsource.MetDataSource):
26
+ """
27
+ ECMWF Integrated Forecasting System (IFS) data source.
28
+
29
+ .. warning::
30
+
31
+ This data source is not fully implemented.
32
+
33
+ Parameters
34
+ ----------
35
+ time : metsource.TimeInput | None
36
+ The time range for data retrieval, either a single datetime or (start, end) datetime range.
37
+ Input must be a single datetime-like or tuple of datetime-like
38
+ (datetime, :class:`pandas.Timestamp`, :class:`numpy.datetime64`)
39
+ specifying the (start, end) of the date range, inclusive.
40
+ If None, all time coordinates will be loaded.
41
+ variables : metsource.VariableInput
42
+ Variable name (i.e. "air_temperature", ["air_temperature, relative_humidity"])
43
+ See :attr:`pressure_level_variables` for the list of available variables.
44
+ pressure_levels : metsource.PressureLevelInput, optional
45
+ Pressure level bounds for data (min, max), in hPa (mbar)
46
+ Set to -1 for to download surface level parameters.
47
+ Defaults to -1.
48
+ paths : str | list[str] | pathlib.Path | list[pathlib.Path] | None, optional
49
+ UNSUPPORTED FOR IFS
50
+ forecast_path: str | pathlib.Path | None, optional
51
+ Path to local forecast files.
52
+ Defaults to None
53
+ forecast_date: DatetimeLike, optional
54
+ Forecast date to load specific netcdf files.
55
+ Defaults to None
56
+
57
+ Notes
58
+ -----
59
+ This takes an average pressure of the model level to create
60
+ pressure level dimensions.
61
+ """
62
+
63
+ __slots__ = ("forecast_date", "forecast_path")
64
+
65
+ #: Root path of IFS data
66
+ forecast_path: pathlib.Path
67
+
68
+ #: Forecast datetime of IFS forecast
69
+ forecast_date: pd.Timestamp
70
+
71
+ def __init__(
72
+ self,
73
+ time: metsource.TimeInput | None,
74
+ variables: metsource.VariableInput,
75
+ pressure_levels: metsource.PressureLevelInput = -1,
76
+ paths: str | list[str] | pathlib.Path | list[pathlib.Path] | None = None,
77
+ grid: float | None = None,
78
+ forecast_path: str | pathlib.Path | None = None,
79
+ forecast_date: DatetimeLike | None = None,
80
+ ) -> None:
81
+ self.paths = paths # TODO: this is currently unused
82
+ self.grid = grid # TODO: this is currently unused
83
+
84
+ # path to forecast files
85
+ if forecast_path is None:
86
+ raise ValueError("Forecast path input is required for IFS")
87
+ self.forecast_path = pathlib.Path(forecast_path)
88
+
89
+ # TODO: automatically select a forecast_date from input time range?
90
+ self.forecast_date = pd.to_datetime(forecast_date).to_pydatetime()
91
+
92
+ # parse inputs
93
+ self.timesteps = metsource.parse_timesteps(time, freq="3h")
94
+ self.pressure_levels = metsource.parse_pressure_levels(pressure_levels, None)
95
+ self.variables = metsource.parse_variables(variables, self.supported_variables)
96
+
97
+ def __repr__(self) -> str:
98
+ base = super().__repr__()
99
+ return f"{base}\n\tForecast date: {self.forecast_date}"
100
+
101
+ @property
102
+ def supported_variables(self) -> list[met.MetVariable]:
103
+ """IFS parameters available.
104
+
105
+ Returns
106
+ -------
107
+ list[MetVariable] | None
108
+ List of MetVariable available in datasource
109
+ """
110
+ return ECMWF_VARIABLES
111
+
112
+ @property
113
+ def supported_pressure_levels(self) -> None:
114
+ """IFS does not provide constant pressure levels and instead uses model levels.
115
+
116
+ Returns
117
+ -------
118
+ list[int]
119
+ """
120
+ return None
121
+
122
+ @overrides
123
+ def open_metdataset(
124
+ self,
125
+ dataset: xr.Dataset | None = None,
126
+ xr_kwargs: dict[str, Any] | None = None,
127
+ **kwargs: Any,
128
+ ) -> met.MetDataset:
129
+ xr_kwargs = xr_kwargs or {}
130
+
131
+ # short-circuit dataset or file paths if provided
132
+ if self.paths is not None or dataset is not None:
133
+ raise NotImplementedError("IFS input paths or input dataset is not supported")
134
+
135
+ # load / merge datasets
136
+ ds = self._open_ifs_dataset(**xr_kwargs)
137
+
138
+ # drop ancillary vars
139
+ ds = ds.drop_vars(names=["hyai", "hybi"])
140
+
141
+ # downselect dataset if only a subset of times, pressure levels, or variables are requested
142
+ if self.timesteps:
143
+ ds = ds.sel(time=self.timesteps)
144
+ else:
145
+ # set timesteps from dataset "time" coordinates
146
+ # np.datetime64 doesn't covert to list[datetime] unless its unit is us
147
+ self.timesteps = ds["time"].values.astype("datetime64[us]").tolist()
148
+
149
+ # downselect hyam/hybm coefficients by the "lev" coordinate
150
+ # (this is a 1-indexed verison of nhym)
151
+ ds["hyam"] = ds["hyam"][dict(nhym=(ds["lev"] - 1).astype(int))]
152
+ ds["hybm"] = ds["hybm"][dict(nhym=(ds["lev"] - 1).astype(int))]
153
+
154
+ # calculate air_pressure (Pa) by hybrid sigma pressure
155
+ ds["air_pressure"] = ds["hyam"] + (ds["hybm"] * ds["surface_pressure"])
156
+ ds["air_pressure"].attrs["units"] = "Pa"
157
+ ds["air_pressure"].attrs["long_name"] = "Air pressure"
158
+
159
+ # calculate virtual temperature (t_virtual)
160
+ # the temperature at which dry air would have the same density
161
+ # as the moist air at a given pressure
162
+ ds["t_virtual"] = ds["t"] * (1 + ds["q"] * ((constants.R_v / constants.R_d) - 1))
163
+ ds["t_virtual"].attrs["units"] = "K"
164
+ ds["t_virtual"].attrs["long_name"] = "Virtual Temperature"
165
+
166
+ # calculate geopotential
167
+ if "z" in self.variable_shortnames:
168
+ ds["z"] = self._calc_geopotential(ds)
169
+
170
+ # take the mean of the air pressure to create quasi-gridded level coordinate
171
+ ds = ds.assign_coords(
172
+ {"level": ("lev", (ds["air_pressure"].mean(dim=["time", "lat", "lon"]) / 100).values)}
173
+ )
174
+ ds = ds.swap_dims({"lev": "level"})
175
+ ds = ds.drop_vars(names=["lev"])
176
+
177
+ # rename dimensions
178
+ ds = ds.rename({"lat": "latitude", "lon": "longitude"})
179
+
180
+ # downselect variables
181
+ ds = ds[self.variable_shortnames]
182
+
183
+ # TODO: fix this correctly
184
+ if "level" not in ds.dims:
185
+ ds = ds.expand_dims({"level": [-1]})
186
+
187
+ # harmonize variable names
188
+ ds = met.standardize_variables(ds, self.variables)
189
+
190
+ self.set_metadata(ds)
191
+ return met.MetDataset(ds, **kwargs)
192
+
193
+ @overrides
194
+ def set_metadata(self, ds: xr.Dataset | met.MetDataset) -> None:
195
+ ds.attrs.update(
196
+ provider="ECMWF",
197
+ dataset="IFS",
198
+ product="forecast",
199
+ )
200
+
201
+ @overrides
202
+ def download_dataset(self, times: list[datetime]) -> None:
203
+ raise NotImplementedError("IFS download is not supported")
204
+
205
+ @overrides
206
+ def cache_dataset(self, dataset: xr.Dataset) -> None:
207
+ raise NotImplementedError("IFS dataset caching not supported")
208
+
209
+ @overrides
210
+ def create_cachepath(self, t: datetime) -> str:
211
+ raise NotImplementedError("IFS download is not supported")
212
+
213
+ def _open_ifs_dataset(self, **xr_kwargs: Any) -> xr.Dataset:
214
+ # get the path to each IFS file for each forecast date
215
+ date_str = self.forecast_date.strftime("%Y%m%d")
216
+ path_full = f"{self.forecast_path}/FC_{date_str}_00_144.nc"
217
+ path_fl = f"{self.forecast_path}/FC_{date_str}_00_144_fl.nc"
218
+ path_surface = f"{self.forecast_path}/FC_{date_str}_00_144_sur.nc"
219
+ path_rad = f"{self.forecast_path}/FC_{date_str}_00_144_rad.nc"
220
+
221
+ # load each dataset
222
+ LOG.debug(f"Loading IFS forecast date {date_str}")
223
+
224
+ # load each dataset
225
+ xr_kwargs.setdefault("chunks", metsource.DEFAULT_CHUNKS)
226
+ xr_kwargs.setdefault("engine", metsource.NETCDF_ENGINE)
227
+ ds_full = xr.open_dataset(path_full, **xr_kwargs)
228
+ ds_fl = xr.open_dataset(path_fl, **xr_kwargs)
229
+ ds_surface = xr.open_dataset(path_surface, **xr_kwargs)
230
+ ds_rad = xr.open_dataset(path_rad, **xr_kwargs)
231
+
232
+ # calculate surface pressure from ln(surface pressure) var, squeeze out "lev" dim
233
+ ds_full["surface_pressure"] = np.exp(ds_full["lnsp"]).squeeze()
234
+ ds_full["surface_pressure"].attrs["units"] = "Pa"
235
+ ds_full["surface_pressure"].attrs["long_name"] = "Surface air pressure"
236
+
237
+ # swap dim names for consistency
238
+ ds_full = ds_full.drop_vars(names=["lnsp", "lev"])
239
+ ds_full = ds_full.rename({"lev_2": "lev"})
240
+
241
+ # drop vars so all datasets can merge
242
+ ds_fl = ds_fl.drop_vars(names=["hyai", "hybi", "hyam", "hybm"])
243
+
244
+ # merge all datasets using the "ds_fl" dimensions as the join keys
245
+ ds = xr.merge([ds_fl, ds_full, ds_surface, ds_rad], join="left") # order matters!
246
+
247
+ return ds
248
+
249
+ def _calc_geopotential(self, ds: xr.Dataset) -> xr.DataArray:
250
+ warnings.warn(
251
+ "The geopotential calculation implementation may assume the underlying grid "
252
+ "starts at ground level. This may not be the case for IFS data. It may be "
253
+ "better to use geometric height (altitude) instead of geopotential for downstream "
254
+ "applications (tau cirrus, etc.).",
255
+ UserWarning,
256
+ )
257
+
258
+ # TODO: this could be done via a mapping on the "lev" dimension
259
+ # groupby("lev")
260
+
261
+ z_level = ds["z"].copy()
262
+ p_level = ds["surface_pressure"].copy()
263
+ geopotential = xr.zeros_like(ds["t"])
264
+
265
+ geopotential.attrs["standard_name"] = "geopotential"
266
+ geopotential.attrs["units"] = "m**2 s**-2"
267
+ geopotential.attrs["long_name"] = "Geopotential"
268
+
269
+ # iterate through level layers from the bottom up
270
+ for k in ds["lev"][::-1]:
271
+ d_log_p = np.log(p_level / ds["air_pressure"].loc[dict(lev=k)])
272
+
273
+ denom = p_level - ds["air_pressure"].loc[dict(lev=k)]
274
+ alpha = 1 - d_log_p * ds["air_pressure"].loc[dict(lev=k)] / denom
275
+
276
+ geopotential.loc[dict(lev=k)] = (
277
+ z_level + ds["t_virtual"].loc[dict(lev=k)] * alpha * constants.R_d
278
+ )
279
+
280
+ # Update values for next loop
281
+ z_level = geopotential.loc[dict(lev=k)].copy()
282
+ p_level = ds["air_pressure"].loc[dict(lev=k)].copy()
283
+
284
+ return geopotential