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.

Files changed (123) hide show
  1. pycontrails/__init__.py +70 -0
  2. pycontrails/_version.py +34 -0
  3. pycontrails/core/__init__.py +30 -0
  4. pycontrails/core/aircraft_performance.py +679 -0
  5. pycontrails/core/airports.py +228 -0
  6. pycontrails/core/cache.py +889 -0
  7. pycontrails/core/coordinates.py +174 -0
  8. pycontrails/core/fleet.py +483 -0
  9. pycontrails/core/flight.py +2185 -0
  10. pycontrails/core/flightplan.py +228 -0
  11. pycontrails/core/fuel.py +140 -0
  12. pycontrails/core/interpolation.py +702 -0
  13. pycontrails/core/met.py +2936 -0
  14. pycontrails/core/met_var.py +387 -0
  15. pycontrails/core/models.py +1321 -0
  16. pycontrails/core/polygon.py +549 -0
  17. pycontrails/core/rgi_cython.cpython-314-darwin.so +0 -0
  18. pycontrails/core/vector.py +2249 -0
  19. pycontrails/datalib/__init__.py +12 -0
  20. pycontrails/datalib/_met_utils/metsource.py +746 -0
  21. pycontrails/datalib/ecmwf/__init__.py +73 -0
  22. pycontrails/datalib/ecmwf/arco_era5.py +345 -0
  23. pycontrails/datalib/ecmwf/common.py +114 -0
  24. pycontrails/datalib/ecmwf/era5.py +554 -0
  25. pycontrails/datalib/ecmwf/era5_model_level.py +490 -0
  26. pycontrails/datalib/ecmwf/hres.py +804 -0
  27. pycontrails/datalib/ecmwf/hres_model_level.py +466 -0
  28. pycontrails/datalib/ecmwf/ifs.py +287 -0
  29. pycontrails/datalib/ecmwf/model_levels.py +435 -0
  30. pycontrails/datalib/ecmwf/static/model_level_dataframe_v20240418.csv +139 -0
  31. pycontrails/datalib/ecmwf/variables.py +268 -0
  32. pycontrails/datalib/geo_utils.py +261 -0
  33. pycontrails/datalib/gfs/__init__.py +28 -0
  34. pycontrails/datalib/gfs/gfs.py +656 -0
  35. pycontrails/datalib/gfs/variables.py +104 -0
  36. pycontrails/datalib/goes.py +764 -0
  37. pycontrails/datalib/gruan.py +343 -0
  38. pycontrails/datalib/himawari/__init__.py +27 -0
  39. pycontrails/datalib/himawari/header_struct.py +266 -0
  40. pycontrails/datalib/himawari/himawari.py +671 -0
  41. pycontrails/datalib/landsat.py +589 -0
  42. pycontrails/datalib/leo_utils/__init__.py +5 -0
  43. pycontrails/datalib/leo_utils/correction.py +266 -0
  44. pycontrails/datalib/leo_utils/landsat_metadata.py +300 -0
  45. pycontrails/datalib/leo_utils/search.py +250 -0
  46. pycontrails/datalib/leo_utils/sentinel_metadata.py +748 -0
  47. pycontrails/datalib/leo_utils/static/bq_roi_query.sql +6 -0
  48. pycontrails/datalib/leo_utils/vis.py +59 -0
  49. pycontrails/datalib/sentinel.py +650 -0
  50. pycontrails/datalib/spire/__init__.py +5 -0
  51. pycontrails/datalib/spire/exceptions.py +62 -0
  52. pycontrails/datalib/spire/spire.py +604 -0
  53. pycontrails/ext/bada.py +42 -0
  54. pycontrails/ext/cirium.py +14 -0
  55. pycontrails/ext/empirical_grid.py +140 -0
  56. pycontrails/ext/synthetic_flight.py +431 -0
  57. pycontrails/models/__init__.py +1 -0
  58. pycontrails/models/accf.py +425 -0
  59. pycontrails/models/apcemm/__init__.py +8 -0
  60. pycontrails/models/apcemm/apcemm.py +983 -0
  61. pycontrails/models/apcemm/inputs.py +226 -0
  62. pycontrails/models/apcemm/static/apcemm_yaml_template.yaml +183 -0
  63. pycontrails/models/apcemm/utils.py +437 -0
  64. pycontrails/models/cocip/__init__.py +29 -0
  65. pycontrails/models/cocip/cocip.py +2742 -0
  66. pycontrails/models/cocip/cocip_params.py +305 -0
  67. pycontrails/models/cocip/cocip_uncertainty.py +291 -0
  68. pycontrails/models/cocip/contrail_properties.py +1530 -0
  69. pycontrails/models/cocip/output_formats.py +2270 -0
  70. pycontrails/models/cocip/radiative_forcing.py +1260 -0
  71. pycontrails/models/cocip/radiative_heating.py +520 -0
  72. pycontrails/models/cocip/unterstrasser_wake_vortex.py +508 -0
  73. pycontrails/models/cocip/wake_vortex.py +396 -0
  74. pycontrails/models/cocip/wind_shear.py +120 -0
  75. pycontrails/models/cocipgrid/__init__.py +9 -0
  76. pycontrails/models/cocipgrid/cocip_grid.py +2552 -0
  77. pycontrails/models/cocipgrid/cocip_grid_params.py +138 -0
  78. pycontrails/models/dry_advection.py +602 -0
  79. pycontrails/models/emissions/__init__.py +21 -0
  80. pycontrails/models/emissions/black_carbon.py +599 -0
  81. pycontrails/models/emissions/emissions.py +1353 -0
  82. pycontrails/models/emissions/ffm2.py +336 -0
  83. pycontrails/models/emissions/static/default-engine-uids.csv +239 -0
  84. pycontrails/models/emissions/static/edb-gaseous-v29b-engines.csv +596 -0
  85. pycontrails/models/emissions/static/edb-nvpm-v29b-engines.csv +215 -0
  86. pycontrails/models/extended_k15.py +1327 -0
  87. pycontrails/models/humidity_scaling/__init__.py +37 -0
  88. pycontrails/models/humidity_scaling/humidity_scaling.py +1075 -0
  89. pycontrails/models/humidity_scaling/quantiles/era5-model-level-quantiles.pq +0 -0
  90. pycontrails/models/humidity_scaling/quantiles/era5-pressure-level-quantiles.pq +0 -0
  91. pycontrails/models/issr.py +210 -0
  92. pycontrails/models/pcc.py +326 -0
  93. pycontrails/models/pcr.py +154 -0
  94. pycontrails/models/ps_model/__init__.py +18 -0
  95. pycontrails/models/ps_model/ps_aircraft_params.py +381 -0
  96. pycontrails/models/ps_model/ps_grid.py +701 -0
  97. pycontrails/models/ps_model/ps_model.py +1000 -0
  98. pycontrails/models/ps_model/ps_operational_limits.py +525 -0
  99. pycontrails/models/ps_model/static/ps-aircraft-params-20250328.csv +69 -0
  100. pycontrails/models/ps_model/static/ps-synonym-list-20250328.csv +104 -0
  101. pycontrails/models/sac.py +442 -0
  102. pycontrails/models/tau_cirrus.py +183 -0
  103. pycontrails/physics/__init__.py +1 -0
  104. pycontrails/physics/constants.py +117 -0
  105. pycontrails/physics/geo.py +1138 -0
  106. pycontrails/physics/jet.py +968 -0
  107. pycontrails/physics/static/iata-cargo-load-factors-20250221.csv +74 -0
  108. pycontrails/physics/static/iata-passenger-load-factors-20250221.csv +74 -0
  109. pycontrails/physics/thermo.py +551 -0
  110. pycontrails/physics/units.py +472 -0
  111. pycontrails/py.typed +0 -0
  112. pycontrails/utils/__init__.py +1 -0
  113. pycontrails/utils/dependencies.py +66 -0
  114. pycontrails/utils/iteration.py +13 -0
  115. pycontrails/utils/json.py +187 -0
  116. pycontrails/utils/temp.py +50 -0
  117. pycontrails/utils/types.py +163 -0
  118. pycontrails-0.59.0.dist-info/METADATA +179 -0
  119. pycontrails-0.59.0.dist-info/RECORD +123 -0
  120. pycontrails-0.59.0.dist-info/WHEEL +6 -0
  121. pycontrails-0.59.0.dist-info/licenses/LICENSE +178 -0
  122. pycontrails-0.59.0.dist-info/licenses/NOTICE +43 -0
  123. pycontrails-0.59.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,287 @@
1
+ """ECWMF IFS forecast data access."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import pathlib
7
+ import sys
8
+ import warnings
9
+ from datetime import datetime
10
+ from typing import Any
11
+
12
+ if sys.version_info >= (3, 12):
13
+ from typing import override
14
+ else:
15
+ from typing_extensions import override
16
+
17
+ LOG = logging.getLogger(__name__)
18
+
19
+ import numpy as np
20
+ import pandas as pd
21
+ import xarray as xr
22
+
23
+ from pycontrails.core import met
24
+ from pycontrails.datalib._met_utils import metsource
25
+ from pycontrails.datalib.ecmwf.variables import ECMWF_VARIABLES
26
+ from pycontrails.physics import constants
27
+ from pycontrails.utils.types import DatetimeLike
28
+
29
+
30
+ class IFS(metsource.MetDataSource):
31
+ """
32
+ ECMWF Integrated Forecasting System (IFS) data source.
33
+
34
+ .. warning::
35
+
36
+ This data source is not fully implemented.
37
+
38
+ Parameters
39
+ ----------
40
+ time : metsource.TimeInput | None
41
+ The time range for data retrieval, either a single datetime or (start, end) datetime range.
42
+ Input must be a single datetime-like or tuple of datetime-like
43
+ (datetime, :class:`pandas.Timestamp`, :class:`numpy.datetime64`)
44
+ specifying the (start, end) of the date range, inclusive.
45
+ If None, all time coordinates will be loaded.
46
+ variables : metsource.VariableInput
47
+ Variable name (i.e. "air_temperature", ["air_temperature, relative_humidity"])
48
+ See :attr:`pressure_level_variables` for the list of available variables.
49
+ pressure_levels : metsource.PressureLevelInput, optional
50
+ Pressure level bounds for data (min, max), in hPa (mbar)
51
+ Set to -1 for to download surface level parameters.
52
+ Defaults to -1.
53
+ paths : str | list[str] | pathlib.Path | list[pathlib.Path] | None, optional
54
+ UNSUPPORTED FOR IFS
55
+ forecast_path: str | pathlib.Path | None, optional
56
+ Path to local forecast files.
57
+ Defaults to None
58
+ forecast_date: DatetimeLike, optional
59
+ Forecast date to load specific netcdf files.
60
+ Defaults to None
61
+
62
+ Notes
63
+ -----
64
+ This takes an average pressure of the model level to create
65
+ pressure level dimensions.
66
+ """
67
+
68
+ __slots__ = ("forecast_date", "forecast_path")
69
+
70
+ #: Root path of IFS data
71
+ forecast_path: pathlib.Path
72
+
73
+ #: Forecast datetime of IFS forecast
74
+ forecast_date: pd.Timestamp
75
+
76
+ def __init__(
77
+ self,
78
+ time: metsource.TimeInput | None,
79
+ variables: metsource.VariableInput,
80
+ pressure_levels: metsource.PressureLevelInput = -1,
81
+ paths: str | list[str] | pathlib.Path | list[pathlib.Path] | None = None,
82
+ grid: float | None = None,
83
+ forecast_path: str | pathlib.Path | None = None,
84
+ forecast_date: DatetimeLike | None = None,
85
+ ) -> None:
86
+ self.paths = paths # TODO: this is currently unused
87
+ self.grid = grid # TODO: this is currently unused
88
+
89
+ # path to forecast files
90
+ if forecast_path is None:
91
+ raise ValueError("Forecast path input is required for IFS")
92
+ self.forecast_path = pathlib.Path(forecast_path)
93
+
94
+ # TODO: automatically select a forecast_date from input time range?
95
+ self.forecast_date = pd.to_datetime(forecast_date).to_pydatetime()
96
+
97
+ # parse inputs
98
+ self.timesteps = metsource.parse_timesteps(time, freq="3h")
99
+ self.pressure_levels = metsource.parse_pressure_levels(pressure_levels, None)
100
+ self.variables = metsource.parse_variables(variables, self.supported_variables)
101
+
102
+ def __repr__(self) -> str:
103
+ base = super().__repr__()
104
+ return f"{base}\n\tForecast date: {self.forecast_date}"
105
+
106
+ @property
107
+ def supported_variables(self) -> list[met.MetVariable]:
108
+ """IFS parameters available.
109
+
110
+ Returns
111
+ -------
112
+ list[MetVariable] | None
113
+ List of MetVariable available in datasource
114
+ """
115
+ return ECMWF_VARIABLES
116
+
117
+ @property
118
+ def supported_pressure_levels(self) -> None:
119
+ """IFS does not provide constant pressure levels and instead uses model levels.
120
+
121
+ Returns
122
+ -------
123
+ list[int]
124
+ """
125
+ return None
126
+
127
+ @override
128
+ def open_metdataset(
129
+ self,
130
+ dataset: xr.Dataset | None = None,
131
+ xr_kwargs: dict[str, Any] | None = None,
132
+ **kwargs: Any,
133
+ ) -> met.MetDataset:
134
+ xr_kwargs = xr_kwargs or {}
135
+
136
+ # short-circuit dataset or file paths if provided
137
+ if self.paths is not None or dataset is not None:
138
+ raise NotImplementedError("IFS input paths or input dataset is not supported")
139
+
140
+ # load / merge datasets
141
+ ds = self._open_ifs_dataset(**xr_kwargs)
142
+
143
+ # drop ancillary vars
144
+ ds = ds.drop_vars(names=["hyai", "hybi"])
145
+
146
+ # downselect dataset if only a subset of times, pressure levels, or variables are requested
147
+ if self.timesteps:
148
+ ds = ds.sel(time=self.timesteps)
149
+ else:
150
+ # set timesteps from dataset "time" coordinates
151
+ # np.datetime64 doesn't covert to list[datetime] unless its unit is us
152
+ self.timesteps = ds["time"].values.astype("datetime64[us]").tolist()
153
+
154
+ # downselect hyam/hybm coefficients by the "lev" coordinate
155
+ # (this is a 1-indexed verison of nhym)
156
+ ds["hyam"] = ds["hyam"][dict(nhym=(ds["lev"] - 1).astype(int))]
157
+ ds["hybm"] = ds["hybm"][dict(nhym=(ds["lev"] - 1).astype(int))]
158
+
159
+ # calculate air_pressure (Pa) by hybrid sigma pressure
160
+ ds["air_pressure"] = ds["hyam"] + (ds["hybm"] * ds["surface_pressure"])
161
+ ds["air_pressure"].attrs["units"] = "Pa"
162
+ ds["air_pressure"].attrs["long_name"] = "Air pressure"
163
+
164
+ # calculate virtual temperature (t_virtual)
165
+ # the temperature at which dry air would have the same density
166
+ # as the moist air at a given pressure
167
+ ds["t_virtual"] = ds["t"] * (1 + ds["q"] * ((constants.R_v / constants.R_d) - 1))
168
+ ds["t_virtual"].attrs["units"] = "K"
169
+ ds["t_virtual"].attrs["long_name"] = "Virtual Temperature"
170
+
171
+ # calculate geopotential
172
+ if "z" in self.variable_shortnames:
173
+ ds["z"] = self._calc_geopotential(ds)
174
+
175
+ # take the mean of the air pressure to create quasi-gridded level coordinate
176
+ ds = ds.assign_coords(
177
+ {"level": ("lev", (ds["air_pressure"].mean(dim=["time", "lat", "lon"]) / 100).values)}
178
+ )
179
+ ds = ds.swap_dims({"lev": "level"})
180
+ ds = ds.drop_vars(names=["lev"])
181
+
182
+ # rename dimensions
183
+ ds = ds.rename({"lat": "latitude", "lon": "longitude"})
184
+
185
+ # downselect variables
186
+ ds = ds[self.variable_shortnames]
187
+
188
+ # TODO: fix this correctly
189
+ if "level" not in ds.dims:
190
+ ds = ds.expand_dims({"level": [-1]})
191
+
192
+ # harmonize variable names
193
+ ds = met.standardize_variables(ds, self.variables)
194
+
195
+ self.set_metadata(ds)
196
+ return met.MetDataset(ds, **kwargs)
197
+
198
+ @override
199
+ def set_metadata(self, ds: xr.Dataset | met.MetDataset) -> None:
200
+ ds.attrs.update(
201
+ provider="ECMWF",
202
+ dataset="IFS",
203
+ product="forecast",
204
+ )
205
+
206
+ @override
207
+ def download_dataset(self, times: list[datetime]) -> None:
208
+ raise NotImplementedError("IFS download is not supported")
209
+
210
+ @override
211
+ def cache_dataset(self, dataset: xr.Dataset) -> None:
212
+ raise NotImplementedError("IFS dataset caching not supported")
213
+
214
+ @override
215
+ def create_cachepath(self, t: datetime) -> str:
216
+ raise NotImplementedError("IFS download is not supported")
217
+
218
+ def _open_ifs_dataset(self, **xr_kwargs: Any) -> xr.Dataset:
219
+ # get the path to each IFS file for each forecast date
220
+ date_str = self.forecast_date.strftime("%Y%m%d")
221
+ path_full = f"{self.forecast_path}/FC_{date_str}_00_144.nc"
222
+ path_fl = f"{self.forecast_path}/FC_{date_str}_00_144_fl.nc"
223
+ path_surface = f"{self.forecast_path}/FC_{date_str}_00_144_sur.nc"
224
+ path_rad = f"{self.forecast_path}/FC_{date_str}_00_144_rad.nc"
225
+
226
+ # load each dataset
227
+ LOG.debug(f"Loading IFS forecast date {date_str}")
228
+
229
+ # load each dataset
230
+ xr_kwargs.setdefault("chunks", metsource.DEFAULT_CHUNKS)
231
+ xr_kwargs.setdefault("engine", metsource.NETCDF_ENGINE)
232
+ ds_full = xr.open_dataset(path_full, **xr_kwargs)
233
+ ds_fl = xr.open_dataset(path_fl, **xr_kwargs)
234
+ ds_surface = xr.open_dataset(path_surface, **xr_kwargs)
235
+ ds_rad = xr.open_dataset(path_rad, **xr_kwargs)
236
+
237
+ # calculate surface pressure from ln(surface pressure) var, squeeze out "lev" dim
238
+ ds_full["surface_pressure"] = np.exp(ds_full["lnsp"]).squeeze()
239
+ ds_full["surface_pressure"].attrs["units"] = "Pa"
240
+ ds_full["surface_pressure"].attrs["long_name"] = "Surface air pressure"
241
+
242
+ # swap dim names for consistency
243
+ ds_full = ds_full.drop_vars(names=["lnsp", "lev"])
244
+ ds_full = ds_full.rename({"lev_2": "lev"})
245
+
246
+ # drop vars so all datasets can merge
247
+ ds_fl = ds_fl.drop_vars(names=["hyai", "hybi", "hyam", "hybm"])
248
+
249
+ # merge all datasets using the "ds_fl" dimensions as the join keys
250
+ return xr.merge([ds_fl, ds_full, ds_surface, ds_rad], join="left") # order matters!
251
+
252
+ def _calc_geopotential(self, ds: xr.Dataset) -> xr.DataArray:
253
+ warnings.warn(
254
+ "The geopotential calculation implementation may assume the underlying grid "
255
+ "starts at ground level. This may not be the case for IFS data. It may be "
256
+ "better to use geometric height (altitude) instead of geopotential for downstream "
257
+ "applications (tau cirrus, etc.).",
258
+ UserWarning,
259
+ )
260
+
261
+ # TODO: this could be done via a mapping on the "lev" dimension
262
+ # groupby("lev")
263
+
264
+ z_level = ds["z"].copy()
265
+ p_level = ds["surface_pressure"].copy()
266
+ geopotential = xr.zeros_like(ds["t"])
267
+
268
+ geopotential.attrs["standard_name"] = "geopotential"
269
+ geopotential.attrs["units"] = "m**2 s**-2"
270
+ geopotential.attrs["long_name"] = "Geopotential"
271
+
272
+ # iterate through level layers from the bottom up
273
+ for k in ds["lev"][::-1]:
274
+ d_log_p = np.log(p_level / ds["air_pressure"].loc[dict(lev=k)])
275
+
276
+ denom = p_level - ds["air_pressure"].loc[dict(lev=k)]
277
+ alpha = 1 - d_log_p * ds["air_pressure"].loc[dict(lev=k)] / denom
278
+
279
+ geopotential.loc[dict(lev=k)] = (
280
+ z_level + ds["t_virtual"].loc[dict(lev=k)] * alpha * constants.R_d
281
+ )
282
+
283
+ # Update values for next loop
284
+ z_level = geopotential.loc[dict(lev=k)].copy()
285
+ p_level = ds["air_pressure"].loc[dict(lev=k)].copy()
286
+
287
+ return geopotential