pycontrails 0.49.4__cp310-cp310-macosx_11_0_arm64.whl → 0.50.0__cp310-cp310-macosx_11_0_arm64.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.

pycontrails/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.49.4'
16
- __version_tuple__ = version_tuple = (0, 49, 4)
15
+ __version__ = version = '0.50.0'
16
+ __version_tuple__ = version_tuple = (0, 50, 0)
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import abc
6
- import dataclasses
7
6
  import hashlib
8
7
  import logging
9
8
  import pathlib
@@ -109,6 +108,11 @@ def parse_pressure_levels(
109
108
  ) -> list[int]:
110
109
  """Check input pressure levels are consistent type and ensure levels exist in ECMWF data source.
111
110
 
111
+ .. versionchanged:: 0.50.0
112
+
113
+ The returned pressure levels are now sorted. Pressure levels must be unique.
114
+ Raises ValueError if pressure levels have mixed signs.
115
+
112
116
  Parameters
113
117
  ----------
114
118
  pressure_levels : PressureLevelInput
@@ -127,18 +131,31 @@ def parse_pressure_levels(
127
131
  ValueError
128
132
  Raises ValueError if pressure level is not supported by ECMWF data source
129
133
  """
130
- # ensure pressure_levels is list-like
134
+ # Ensure pressure_levels is array-like
131
135
  if isinstance(pressure_levels, (int, float)):
132
136
  pressure_levels = [pressure_levels]
133
137
 
134
- # Cast array-like to list of ints
135
- out = np.asarray(pressure_levels, dtype=int).tolist()
138
+ # Cast array-like to int dtype and sort
139
+ arr = np.asarray(pressure_levels, dtype=int)
140
+ arr.sort()
136
141
 
137
- # ensure pressure levels are valid
138
- for pl in out:
139
- if supported and pl not in supported:
140
- msg = f"Pressure level {pl} is not supported. Supported levels: {supported}"
141
- raise ValueError(msg)
142
+ # If any values are non-positive, the entire array should be [-1]
143
+ if np.any(arr <= 0) and not np.array_equal(arr, [-1]):
144
+ msg = f"Pressure levels must be all positive or all -1, got {arr}"
145
+ raise ValueError(msg)
146
+
147
+ # Ensure pressure levels are unique
148
+ if np.any(np.diff(arr) == 0):
149
+ msg = f"Pressure levels must be unique, got {arr}"
150
+ raise ValueError(msg)
151
+
152
+ out = arr.tolist()
153
+ if supported is None:
154
+ return out
155
+
156
+ if missing := set(out).difference(supported):
157
+ msg = f"Pressure levels {sorted(missing)} are not supported. Supported levels: {supported}"
158
+ raise ValueError(msg)
142
159
 
143
160
  return out
144
161
 
@@ -146,6 +163,11 @@ def parse_pressure_levels(
146
163
  def parse_variables(variables: VariableInput, supported: list[MetVariable]) -> list[MetVariable]:
147
164
  """Parse input variables.
148
165
 
166
+ .. versionchanged:: 0.50.0
167
+
168
+ The output is no longer copied. Each :class:`MetVariable` is a frozen dataclass,
169
+ so copying is unnecessary.
170
+
149
171
  Parameters
150
172
  ----------
151
173
  variables : VariableInput
@@ -178,35 +200,31 @@ def parse_variables(variables: VariableInput, supported: list[MetVariable]) -> l
178
200
  else:
179
201
  parsed_variables = variables
180
202
 
181
- # unpack dict of supported str values from supported
182
203
  short_names = {v.short_name: v for v in supported}
183
204
  standard_names = {v.standard_name: v for v in supported}
184
205
  long_names = {v.long_name: v for v in supported}
185
-
186
- # unpack dict of support int values from supported
187
206
  ecmwf_ids = {v.ecmwf_id: v for v in supported}
188
207
  grib1_ids = {v.grib1_id: v for v in supported}
208
+ supported_set = set(supported)
189
209
 
190
210
  for var in parsed_variables:
191
211
  matched = _find_match(
192
212
  var,
193
- supported,
213
+ supported_set,
194
214
  ecmwf_ids, # type: ignore[arg-type]
195
215
  grib1_ids, # type: ignore[arg-type]
196
216
  short_names,
197
217
  standard_names,
198
218
  long_names, # type: ignore[arg-type]
199
219
  )
200
-
201
- # "replace" copies dataclass
202
- met_var_list.append(dataclasses.replace(matched))
220
+ met_var_list.append(matched)
203
221
 
204
222
  return met_var_list
205
223
 
206
224
 
207
225
  def _find_match(
208
226
  var: VariableInput,
209
- supported: list[MetVariable],
227
+ supported: set[MetVariable],
210
228
  ecmwf_ids: dict[int, MetVariable],
211
229
  grib1_ids: dict[int, MetVariable],
212
230
  short_names: dict[str, MetVariable],
@@ -215,9 +233,8 @@ def _find_match(
215
233
  ) -> MetVariable:
216
234
  """Find a match for input variable in supported."""
217
235
 
218
- if isinstance(var, MetVariable):
219
- if var in supported:
220
- return var
236
+ if isinstance(var, MetVariable) and var in supported:
237
+ return var
221
238
 
222
239
  # list of MetVariable options
223
240
  # here we extract the first MetVariable in var that is supported
@@ -230,21 +247,19 @@ def _find_match(
230
247
  if v in supported:
231
248
  return v
232
249
 
233
- # int code
234
250
  elif isinstance(var, int):
235
- if var in ecmwf_ids:
236
- return ecmwf_ids[var]
237
- if var in grib1_ids:
238
- return grib1_ids[var]
251
+ if ret := ecmwf_ids.get(var):
252
+ return ret
253
+ if ret := grib1_ids.get(var):
254
+ return ret
239
255
 
240
- # string reference
241
256
  elif isinstance(var, str):
242
- if var in short_names:
243
- return short_names[var]
244
- if var in standard_names:
245
- return standard_names[var]
246
- if var in long_names:
247
- return long_names[var]
257
+ if ret := short_names.get(var):
258
+ return ret
259
+ if ret := standard_names.get(var):
260
+ return ret
261
+ if ret := long_names.get(var):
262
+ return ret
248
263
 
249
264
  msg = f"{var} is not in supported parameters. Supported parameters include: {standard_names}"
250
265
  raise ValueError(msg)
@@ -395,6 +410,14 @@ class MetDataSource(abc.ABC):
395
410
  """
396
411
  return [v.standard_name for v in self.variables]
397
412
 
413
+ @property
414
+ def is_single_level(self) -> bool:
415
+ """Return True if the datasource is single level data.
416
+
417
+ .. versionadded:: 0.50.0
418
+ """
419
+ return self.pressure_levels == [-1]
420
+
398
421
  @property
399
422
  def pressure_level_variables(self) -> list[MetVariable]:
400
423
  """Parameters available from data source.
@@ -426,10 +449,9 @@ class MetDataSource(abc.ABC):
426
449
  list[MetVariable] | None
427
450
  List of MetVariable available in datasource
428
451
  """
429
- if self.pressure_levels != [-1]:
430
- return self.pressure_level_variables
431
-
432
- return self.single_level_variables
452
+ return (
453
+ self.single_level_variables if self.is_single_level else self.pressure_level_variables
454
+ )
433
455
 
434
456
  @property
435
457
  def supported_pressure_levels(self) -> list[int] | None:
@@ -497,7 +519,7 @@ class MetDataSource(abc.ABC):
497
519
  def open_metdataset(
498
520
  self,
499
521
  dataset: xr.Dataset | None = None,
500
- xr_kwargs: dict[str, int] | None = None,
522
+ xr_kwargs: dict[str, Any] | None = None,
501
523
  **kwargs: Any,
502
524
  ) -> MetDataset:
503
525
  """Open MetDataset from data source.
@@ -510,7 +532,7 @@ class MetDataSource(abc.ABC):
510
532
  dataset : xr.Dataset | None, optional
511
533
  Input :class:`xr.Dataset` loaded manually.
512
534
  The dataset must have the same format as the original data source API or files.
513
- xr_kwargs : dict[str, int] | None, optional
535
+ xr_kwargs : dict[str, Any] | None, optional
514
536
  Dictionary of keyword arguments passed into :func:`xarray.open_mfdataset`
515
537
  when opening files. Examples include "chunks", "engine", "parallel", etc.
516
538
  Ignored if ``dataset`` is input.
pycontrails/core/met.py CHANGED
@@ -1065,8 +1065,8 @@ class MetDataset(MetBase):
1065
1065
 
1066
1066
  """
1067
1067
  coords_keys = self.data.dims
1068
- variables = self.indexes
1069
- coords_vals = [variables[key].values for key in coords_keys]
1068
+ indexes = self.indexes
1069
+ coords_vals = [indexes[key].values for key in coords_keys]
1070
1070
  coords_meshes = np.meshgrid(*coords_vals, indexing="ij")
1071
1071
  raveled_coords = (mesh.ravel() for mesh in coords_meshes)
1072
1072
  data = dict(zip(coords_keys, raveled_coords))
@@ -1181,12 +1181,12 @@ class MetDataset(MetBase):
1181
1181
  level: npt.ArrayLike | float,
1182
1182
  time: npt.ArrayLike | np.datetime64,
1183
1183
  ) -> MetDataset:
1184
- """Create a :class:`MetDataset` containing a coordinate skeleton from coordinate arrays.
1184
+ r"""Create a :class:`MetDataset` containing a coordinate skeleton from coordinate arrays.
1185
1185
 
1186
1186
  Parameters
1187
1187
  ----------
1188
1188
  longitude, latitude : npt.ArrayLike | float
1189
- Horizontal coordinates, in [:math:`degrees`]
1189
+ Horizontal coordinates, in [:math:`\deg`]
1190
1190
  level : npt.ArrayLike | float
1191
1191
  Vertical coordinate, in [:math:`hPa`]
1192
1192
  time: npt.ArrayLike | np.datetime64,
@@ -76,12 +76,12 @@ class MetVariable:
76
76
 
77
77
  @property
78
78
  def ecmwf_link(self) -> str | None:
79
- """Database link in the ECMWF Paramter Database if :attr:`ecmwf_id` is defined.
79
+ """Database link in the ECMWF Parameter Database if :attr:`ecmwf_id` is defined.
80
80
 
81
81
  Returns
82
82
  -------
83
83
  str
84
- Database link in the ECMWF Paramter Database
84
+ Database link in the ECMWF Parameter Database
85
85
  """
86
86
  return (
87
87
  f"https://apps.ecmwf.int/codes/grib/param-db?id={self.ecmwf_id}"
@@ -440,17 +440,21 @@ class Model(ABC):
440
440
  # Return dataset with the same coords as self.met, but empty data_vars
441
441
  return MetDataset(xr.Dataset(coords=self.met.data.coords))
442
442
 
443
+ copy_source = self.params["copy_source"]
444
+
443
445
  # Turn Sequence into Fleet
444
446
  if isinstance(source, Sequence):
445
- # TODO: fix type guard here
446
- return Fleet.from_seq(source, copy=self.params["copy_source"])
447
+ if not copy_source:
448
+ msg = "Parameter copy_source=False is not supported for Sequence[Flight] source"
449
+ raise ValueError(msg)
450
+ return Fleet.from_seq(source)
447
451
 
448
452
  # Raise error if source is not a MetDataset or GeoVectorDataset
449
453
  if not isinstance(source, (MetDataset, GeoVectorDataset)):
450
454
  msg = f"Unknown source type: {type(source)}"
451
455
  raise TypeError(msg)
452
456
 
453
- if self.params["copy_source"]:
457
+ if copy_source:
454
458
  source = source.copy()
455
459
 
456
460
  if not isinstance(source, Flight):
@@ -1899,19 +1899,19 @@ class GeoVectorDataset(VectorDataset):
1899
1899
  MetDataset | MetDataArray
1900
1900
  Copy of downselected MetDataset or MetDataArray.
1901
1901
  """
1902
- variables = met.indexes
1902
+ indexes = met.indexes
1903
1903
  lon_slice = coordinates.slice_domain(
1904
- variables["longitude"].to_numpy(),
1904
+ indexes["longitude"].to_numpy(),
1905
1905
  self["longitude"],
1906
1906
  buffer=longitude_buffer,
1907
1907
  )
1908
1908
  lat_slice = coordinates.slice_domain(
1909
- variables["latitude"].to_numpy(),
1909
+ indexes["latitude"].to_numpy(),
1910
1910
  self["latitude"],
1911
1911
  buffer=latitude_buffer,
1912
1912
  )
1913
1913
  time_slice = coordinates.slice_domain(
1914
- variables["time"].to_numpy(),
1914
+ indexes["time"].to_numpy(),
1915
1915
  self["time"],
1916
1916
  buffer=time_buffer,
1917
1917
  )
@@ -1921,7 +1921,7 @@ class GeoVectorDataset(VectorDataset):
1921
1921
  level_slice = slice(None)
1922
1922
  else:
1923
1923
  level_slice = coordinates.slice_domain(
1924
- variables["level"].to_numpy(),
1924
+ indexes["level"].to_numpy(),
1925
1925
  self.level,
1926
1926
  buffer=level_buffer,
1927
1927
  )
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from pycontrails.datalib.ecmwf.arco_era5 import ARCOERA5
5
6
  from pycontrails.datalib.ecmwf.era5 import ERA5
6
7
  from pycontrails.datalib.ecmwf.hres import HRES
7
8
  from pycontrails.datalib.ecmwf.ifs import IFS
@@ -11,6 +12,7 @@ from pycontrails.datalib.ecmwf.variables import (
11
12
  SURFACE_VARIABLES,
12
13
  CloudAreaFraction,
13
14
  CloudAreaFractionInLayer,
15
+ Divergence,
14
16
  PotentialVorticity,
15
17
  RelativeHumidity,
16
18
  RelativeVorticity,
@@ -23,11 +25,13 @@ from pycontrails.datalib.ecmwf.variables import (
23
25
  )
24
26
 
25
27
  __all__ = [
28
+ "ARCOERA5",
26
29
  "ERA5",
27
30
  "HRES",
28
31
  "IFS",
29
32
  "CloudAreaFraction",
30
33
  "CloudAreaFractionInLayer",
34
+ "Divergence",
31
35
  "PotentialVorticity",
32
36
  "RelativeHumidity",
33
37
  "RelativeVorticity",