pycontrails 0.54.5__cp311-cp311-macosx_11_0_arm64.whl → 0.54.7__cp311-cp311-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.

Files changed (42) hide show
  1. pycontrails/__init__.py +1 -1
  2. pycontrails/_version.py +2 -2
  3. pycontrails/core/aircraft_performance.py +46 -46
  4. pycontrails/core/airports.py +7 -5
  5. pycontrails/core/flight.py +6 -8
  6. pycontrails/core/flightplan.py +11 -11
  7. pycontrails/core/met.py +41 -33
  8. pycontrails/core/met_var.py +80 -0
  9. pycontrails/core/models.py +80 -3
  10. pycontrails/core/rgi_cython.cpython-311-darwin.so +0 -0
  11. pycontrails/core/vector.py +66 -0
  12. pycontrails/datalib/_met_utils/metsource.py +1 -1
  13. pycontrails/datalib/ecmwf/era5.py +5 -6
  14. pycontrails/datalib/ecmwf/era5_model_level.py +4 -5
  15. pycontrails/datalib/ecmwf/ifs.py +1 -3
  16. pycontrails/datalib/gfs/gfs.py +1 -3
  17. pycontrails/datalib/spire/__init__.py +5 -0
  18. pycontrails/datalib/spire/exceptions.py +62 -0
  19. pycontrails/datalib/spire/spire.py +606 -0
  20. pycontrails/models/accf.py +4 -4
  21. pycontrails/models/cocip/cocip.py +116 -19
  22. pycontrails/models/cocip/cocip_params.py +10 -1
  23. pycontrails/models/cocip/output_formats.py +1 -0
  24. pycontrails/models/cocip/unterstrasser_wake_vortex.py +132 -30
  25. pycontrails/models/cocipgrid/cocip_grid.py +3 -0
  26. pycontrails/models/dry_advection.py +51 -19
  27. pycontrails/models/emissions/black_carbon.py +19 -14
  28. pycontrails/models/emissions/emissions.py +8 -8
  29. pycontrails/models/humidity_scaling/humidity_scaling.py +1 -1
  30. pycontrails/models/pcc.py +1 -2
  31. pycontrails/models/ps_model/ps_model.py +3 -31
  32. pycontrails/models/ps_model/ps_operational_limits.py +2 -6
  33. pycontrails/models/tau_cirrus.py +13 -6
  34. pycontrails/physics/constants.py +2 -1
  35. pycontrails/physics/geo.py +3 -3
  36. {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/METADATA +5 -6
  37. {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/NOTICE +1 -1
  38. {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/RECORD +41 -39
  39. {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/WHEEL +1 -1
  40. pycontrails/datalib/spire.py +0 -739
  41. {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/LICENSE +0 -0
  42. {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/top_level.txt +0 -0
pycontrails/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  ``pycontrails`` public API.
3
3
 
4
- Copyright 2021-present Breakthrough Energy
4
+ Copyright 2021-present Contrails.org and the Breakthrough Energy Foundation
5
5
 
6
6
  Licensed under the Apache License, Version 2.0 (the "License");
7
7
  you may not use this file except in compliance with the License.
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.54.5'
16
- __version_tuple__ = version_tuple = (0, 54, 5)
15
+ __version__ = version = '0.54.7'
16
+ __version_tuple__ = version_tuple = (0, 54, 7)
@@ -71,6 +71,8 @@ class AircraftPerformanceParams(ModelParams, CommonAircraftPerformanceParams):
71
71
  #: level with zero wind when computing true airspeed. In other words,
72
72
  #: approximate low-altitude true airspeed with the ground speed. Enabling
73
73
  #: this does NOT remove any NaN values in the ``met`` data itself.
74
+ #: In the case that ``met`` is not provided, any missing values are
75
+ #: filled with zero wind.
74
76
  fill_low_altitude_with_zero_wind: bool = False
75
77
 
76
78
 
@@ -96,22 +98,52 @@ class AircraftPerformance(Model):
96
98
 
97
99
  source: Flight
98
100
 
99
- @abc.abstractmethod
100
101
  @overload
101
102
  def eval(self, source: Fleet, **params: Any) -> Fleet: ...
102
103
 
103
- @abc.abstractmethod
104
104
  @overload
105
105
  def eval(self, source: Flight, **params: Any) -> Flight: ...
106
106
 
107
- @abc.abstractmethod
108
107
  @overload
109
108
  def eval(self, source: None = ..., **params: Any) -> NoReturn: ...
110
109
 
111
- @abc.abstractmethod
112
110
  def eval(self, source: Flight | None = None, **params: Any) -> Flight:
113
111
  """Evaluate the aircraft performance model.
114
112
 
113
+ Parameters
114
+ ----------
115
+ source : Flight
116
+ Flight trajectory to evaluate. Can be a :class:`Flight` or :class:`Fleet`.
117
+ params : Any
118
+ Override :attr:`params` with keyword arguments.
119
+
120
+ Returns
121
+ -------
122
+ Flight
123
+ Flight trajectory with aircraft performance data.
124
+ """
125
+ self.update_params(params)
126
+ self.set_source(source)
127
+ self.source = self.require_source_type(Flight)
128
+ self.downselect_met()
129
+ self.set_source_met()
130
+ self._cleanup_indices()
131
+
132
+ # Calculate true airspeed if not included on source
133
+ self.ensure_true_airspeed_on_source()
134
+
135
+ if isinstance(self.source, Fleet):
136
+ fls = [self.eval_flight(fl) for fl in self.source.to_flight_list()]
137
+ self.source = Fleet.from_seq(fls, attrs=self.source.attrs, broadcast_numeric=False)
138
+ return self.source
139
+
140
+ self.source = self.eval_flight(self.source)
141
+ return self.source
142
+
143
+ @abc.abstractmethod
144
+ def eval_flight(self, fl: Flight) -> Flight:
145
+ """Evaluate the aircraft performance model on a single flight trajectory.
146
+
115
147
  The implementing model adds the following fields to the source flight:
116
148
 
117
149
  - ``aircraft_mass``: aircraft mass at each waypoint, [:math:`kg`]
@@ -128,18 +160,6 @@ class AircraftPerformance(Model):
128
160
  - ``max_mach``: maximum Mach number
129
161
  - ``max_altitude``: maximum altitude, [:math:`m`]
130
162
  - ``total_fuel_burn``: total fuel burn, [:math:`kg`]
131
-
132
- Parameters
133
- ----------
134
- source : Flight
135
- Flight trajectory to evaluate.
136
- params : Any
137
- Override :attr:`params` with keyword arguments.
138
-
139
- Returns
140
- -------
141
- Flight
142
- Flight trajectory with aircraft performance data.
143
163
  """
144
164
 
145
165
  @override
@@ -491,7 +511,8 @@ class AircraftPerformance(Model):
491
511
  tas[cond] = self.source.segment_groundspeed()[cond]
492
512
  return tas
493
513
 
494
- wind_available = ("eastward_wind" in self.source and "northward_wind" in self.source) or (
514
+ # Use current cocip convention: eastward_wind on met, u_wind on source
515
+ wind_available = ("u_wind" in self.source and "v_wind" in self.source) or (
495
516
  self.met is not None and "eastward_wind" in self.met and "northward_wind" in self.met
496
517
  )
497
518
 
@@ -508,12 +529,16 @@ class AircraftPerformance(Model):
508
529
  )
509
530
  raise ValueError(msg)
510
531
 
511
- u = interpolate_met(self.met, self.source, "eastward_wind", **self.interp_kwargs)
512
- v = interpolate_met(self.met, self.source, "northward_wind", **self.interp_kwargs)
532
+ u = interpolate_met(self.met, self.source, "eastward_wind", "u_wind", **self.interp_kwargs)
533
+ v = interpolate_met(self.met, self.source, "northward_wind", "v_wind", **self.interp_kwargs)
513
534
 
514
535
  if fill_with_groundspeed:
515
- met_level_max = self.met.data["level"][-1].item() # type: ignore[union-attr]
516
- cond = self.source.level > met_level_max
536
+ if self.met is None:
537
+ cond = np.isnan(u) & np.isnan(v)
538
+ else:
539
+ met_level_max = self.met.data["level"][-1].item() # type: ignore[union-attr]
540
+ cond = self.source.level > met_level_max
541
+
517
542
  # We DON'T overwrite the original u and v arrays already attached to the source
518
543
  u = np.where(cond, 0.0, u)
519
544
  v = np.where(cond, 0.0, v)
@@ -639,28 +664,3 @@ def _fill_low_altitude_with_isa_temperature(vector: GeoVectorDataset, met_level_
639
664
 
640
665
  t_isa = vector.T_isa()
641
666
  air_temperature[cond] = t_isa[cond]
642
-
643
-
644
- def _fill_low_altitude_tas_with_true_groundspeed(fl: Flight, met_level_max: float) -> None:
645
- """Fill low-altitude NaN values in ``true_airspeed`` with ground speed.
646
-
647
- The ``true_airspeed`` param is assumed to have been computed by
648
- interpolating against a gridded wind field that did not necessarily
649
- extend to the surface. This function fills points below the lowest
650
- altitude in the gridded data with ground speed values.
651
-
652
- This function operates in-place and modifies the ``true_airspeed`` field.
653
-
654
- Parameters
655
- ----------
656
- fl : Flight
657
- Flight instance associated with the ``true_airspeed`` data.
658
- met_level_max : float
659
- The maximum level in the met data, [:math:`hPa`].
660
- """
661
- tas = fl["true_airspeed"]
662
- is_nan = np.isnan(tas)
663
- low_alt = fl.level > met_level_max
664
- cond = is_nan & low_alt
665
-
666
- tas[cond] = fl.segment_groundspeed()[cond]
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import functools
6
+
5
7
  import numpy as np
6
8
  import pandas as pd
7
9
 
@@ -35,6 +37,7 @@ def _download_ourairports_csv() -> pd.DataFrame:
35
37
  )
36
38
 
37
39
 
40
+ @functools.cache
38
41
  def global_airport_database(
39
42
  cachestore: cache.CacheStore | None = None, update_cache: bool = False
40
43
  ) -> pd.DataFrame:
@@ -91,7 +94,7 @@ def global_airport_database(
91
94
  airports = airports.rename(
92
95
  columns={"latitude_deg": "latitude", "longitude_deg": "longitude", "gps_code": "icao_code"},
93
96
  )
94
- airports.fillna({"elevation_ft": 0}, inplace=True)
97
+ airports.fillna({"elevation_ft": 0.0}, inplace=True)
95
98
 
96
99
  # Keep specific airport types used by commercial aviation
97
100
  subset = ("large_airport", "medium_airport", "small_airport", "heliport")
@@ -162,7 +165,7 @@ def find_nearest_airport(
162
165
  ) & airports["latitude"].between((latitude - bbox), (latitude + bbox))
163
166
 
164
167
  # Find the nearest airport from largest to smallest airport type
165
- search_priority = ["large_airport", "medium_airport", "small_airport"]
168
+ search_priority = ("large_airport", "medium_airport", "small_airport")
166
169
 
167
170
  for airport_type in search_priority:
168
171
  is_airport_type = airports["type"] == airport_type
@@ -171,7 +174,7 @@ def find_nearest_airport(
171
174
  if len(nearest_airports) == 1:
172
175
  return nearest_airports["icao_code"].values[0]
173
176
 
174
- elif len(nearest_airports) > 1:
177
+ if len(nearest_airports) > 1:
175
178
  distance = distance_to_airports(
176
179
  nearest_airports,
177
180
  longitude,
@@ -181,8 +184,7 @@ def find_nearest_airport(
181
184
  i_nearest = np.argmin(distance)
182
185
  return nearest_airports["icao_code"].values[i_nearest]
183
186
 
184
- else:
185
- continue
187
+ continue
186
188
 
187
189
  return None
188
190
 
@@ -891,7 +891,7 @@ class Flight(GeoVectorDataset):
891
891
  """
892
892
  methods = "geodesic", "linear"
893
893
  if fill_method not in methods:
894
- raise ValueError(f'Unknown `fill_method`. Supported methods: {", ".join(methods)}')
894
+ raise ValueError(f"Unknown `fill_method`. Supported methods: {', '.join(methods)}")
895
895
 
896
896
  # STEP 0: If self is empty, return an empty flight
897
897
  if not self:
@@ -1388,7 +1388,7 @@ class Flight(GeoVectorDataset):
1388
1388
 
1389
1389
  jump_indices = _antimeridian_index(pd.Series(self["longitude"]))
1390
1390
 
1391
- def _group_to_feature(group: pd.DataFrame) -> dict[str, str | dict[str, Any]]:
1391
+ def _group_to_feature(name: str, group: pd.DataFrame) -> dict[str, str | dict[str, Any]]:
1392
1392
  # assigns a different value to each group of consecutive indices
1393
1393
  subgrouping = group.index.to_series().diff().ne(1).cumsum()
1394
1394
 
@@ -1405,7 +1405,7 @@ class Flight(GeoVectorDataset):
1405
1405
  geometry = {"type": "MultiLineString", "coordinates": multi_ls}
1406
1406
 
1407
1407
  # adding in static properties
1408
- properties: dict[str, Any] = {key: group.name} if key is not None else {}
1408
+ properties: dict[str, Any] = {key: name} if key is not None else {}
1409
1409
  properties.update(self.constants)
1410
1410
  return {"type": "Feature", "geometry": geometry, "properties": properties}
1411
1411
 
@@ -1415,11 +1415,11 @@ class Flight(GeoVectorDataset):
1415
1415
  # create a single group containing all rows of dataframe
1416
1416
  groups = self.dataframe.groupby(lambda _: 0)
1417
1417
 
1418
- features = groups.apply(_group_to_feature, include_groups=False).values.tolist()
1418
+ features = [_group_to_feature(*name_group) for name_group in groups]
1419
1419
  return {"type": "FeatureCollection", "features": features}
1420
1420
 
1421
1421
  def to_traffic(self) -> traffic.core.Flight:
1422
- """Convert to :class:`traffic.core.Flight`instance.
1422
+ """Convert to :class:`traffic.core.Flight` instance.
1423
1423
 
1424
1424
  Returns
1425
1425
  -------
@@ -1982,9 +1982,7 @@ def filter_altitude(
1982
1982
  result[i0:i1] = altitude_filt[i0:i1]
1983
1983
 
1984
1984
  # reapply Savitzky-Golay filter to smooth climb and descent
1985
- result = _sg_filter(result, window_length=kernel_size)
1986
-
1987
- return result
1985
+ return _sg_filter(result, window_length=kernel_size)
1988
1986
 
1989
1987
 
1990
1988
  def segment_duration(
@@ -21,24 +21,24 @@ def to_atc_plan(plan: dict[str, Any]) -> str:
21
21
  --------
22
22
  :func:`parse_atc_plan`
23
23
  """
24
- ret = f'(FPL-{plan["callsign"]}-{plan["flight_rules"]}'
25
- ret += f'{plan["type_of_flight"]}\n'
24
+ ret = f"(FPL-{plan['callsign']}-{plan['flight_rules']}"
25
+ ret += f"{plan['type_of_flight']}\n"
26
26
  ret += "-"
27
27
  if "number_aircraft" in plan and plan["number_aircraft"] <= 10:
28
28
  ret += plan["number_aircraft"]
29
- ret += f'{plan["type_of_aircraft"]}/{plan["wake_category"]}-'
30
- ret += f'{plan["equipment"]}/{plan["transponder"]}\n'
31
- ret += f'-{plan["departure_icao"]}{plan["time"]}\n'
32
- ret += f'-{plan["speed_type"]}{plan["speed"]}{plan["level_type"]}'
33
- ret += f'{plan["level"]} {plan["route"]}\n'
29
+ ret += f"{plan['type_of_aircraft']}/{plan['wake_category']}-"
30
+ ret += f"{plan['equipment']}/{plan['transponder']}\n"
31
+ ret += f"-{plan['departure_icao']}{plan['time']}\n"
32
+ ret += f"-{plan['speed_type']}{plan['speed']}{plan['level_type']}"
33
+ ret += f"{plan['level']} {plan['route']}\n"
34
34
  if "destination_icao" in plan and "duration" in plan:
35
- ret += f'-{plan["destination_icao"]}{plan["duration"]}'
35
+ ret += f"-{plan['destination_icao']}{plan['duration']}"
36
36
  if "alt_icao" in plan:
37
- ret += f' {plan["alt_icao"]}'
37
+ ret += f" {plan['alt_icao']}"
38
38
  if "second_alt_icao" in plan:
39
- ret += f' {plan["second_alt_icao"]}'
39
+ ret += f" {plan['second_alt_icao']}"
40
40
  ret += "\n"
41
- ret += f'-{plan["other_info"]})\n'
41
+ ret += f"-{plan['other_info']})\n"
42
42
  if "supplementary_info" in plan:
43
43
  ret += " ".join([f"{i[0]}/{i[1]}" for i in plan["supplementary_info"].items()])
44
44
 
pycontrails/core/met.py CHANGED
@@ -150,11 +150,8 @@ class MetBase(ABC, Generic[XArrayType]):
150
150
  """
151
151
  longitude = self.indexes["longitude"].to_numpy()
152
152
  if longitude.dtype != COORD_DTYPE:
153
- raise ValueError(
154
- "Longitude values must be of type float64. "
155
- "Instantiate with 'copy=True' to convert to float64. "
156
- "Instantiate with 'validate=False' to skip validation."
157
- )
153
+ msg = f"Longitude values must have dtype {COORD_DTYPE}. Instantiate with 'copy=True'."
154
+ raise ValueError(msg)
158
155
 
159
156
  if self.is_wrapped:
160
157
  # Relax verification if the longitude has already been processed and wrapped
@@ -194,11 +191,8 @@ class MetBase(ABC, Generic[XArrayType]):
194
191
  """
195
192
  latitude = self.indexes["latitude"].to_numpy()
196
193
  if latitude.dtype != COORD_DTYPE:
197
- raise ValueError(
198
- "Latitude values must be of type float64. "
199
- "Instantiate with 'copy=True' to convert to float64. "
200
- "Instantiate with 'validate=False' to skip validation."
201
- )
194
+ msg = f"Latitude values must have dtype {COORD_DTYPE}. Instantiate with 'copy=True'."
195
+ raise ValueError(msg)
202
196
 
203
197
  if latitude[0] < -90.0:
204
198
  raise ValueError(
@@ -233,8 +227,8 @@ class MetBase(ABC, Generic[XArrayType]):
233
227
  if da.dims != self.dim_order:
234
228
  if key is not None:
235
229
  msg = (
236
- f"Data dimension not transposed on variable '{key}'. Instantiate with"
237
- " 'copy=True'."
230
+ f"Data dimension not transposed on variable '{key}'. "
231
+ "Instantiate with 'copy=True'."
238
232
  )
239
233
  else:
240
234
  msg = "Data dimension not transposed. Instantiate with 'copy=True'."
@@ -258,11 +252,8 @@ class MetBase(ABC, Generic[XArrayType]):
258
252
  self._validate_latitude()
259
253
  self._validate_transpose()
260
254
  if self.data["level"].dtype != COORD_DTYPE:
261
- raise ValueError(
262
- "Level values must be of type float64. "
263
- "Instantiate with 'copy=True' to convert to float64. "
264
- "Instantiate with 'validate=False' to skip validation."
265
- )
255
+ msg = f"Level values must have dtype {COORD_DTYPE}. Instantiate with 'copy=True'."
256
+ raise ValueError(msg)
266
257
 
267
258
  def _preprocess_dims(self, wrap_longitude: bool) -> None:
268
259
  """Confirm DataArray or Dataset include required dimension in a consistent format.
@@ -435,7 +426,7 @@ class MetBase(ABC, Generic[XArrayType]):
435
426
  Assumes the longitude dimension is sorted (this is established by the
436
427
  :class:`MetDataset` or :class:`MetDataArray` constructor).
437
428
 
438
- .. versionchanged 0.26.0::
429
+ .. versionchanged:: 0.26.0
439
430
 
440
431
  The previous implementation checked for the minimum and maximum longitude
441
432
  dimension values to be duplicated. The current implementation only checks for
@@ -492,7 +483,7 @@ class MetBase(ABC, Generic[XArrayType]):
492
483
 
493
484
  Does not yet save in parallel.
494
485
 
495
- .. versionchanged::0.34.1
486
+ .. versionchanged:: 0.34.1
496
487
 
497
488
  If :attr:`cachestore` is None, this method assigns it
498
489
  to new :class:`DiskCacheStore`.
@@ -1178,19 +1169,45 @@ class MetDataset(MetBase):
1178
1169
  }
1179
1170
  return self._get_pycontrails_attr_template("product", supported, examples)
1180
1171
 
1181
- def standardize_variables(self, variables: Iterable[MetVariable]) -> None:
1182
- """Standardize variables **in-place**.
1172
+ @overload
1173
+ def standardize_variables(
1174
+ self, variables: Iterable[MetVariable], inplace: Literal[False] = ...
1175
+ ) -> Self: ...
1176
+
1177
+ @overload
1178
+ def standardize_variables(
1179
+ self, variables: Iterable[MetVariable], inplace: Literal[True]
1180
+ ) -> None: ...
1181
+
1182
+ def standardize_variables(
1183
+ self, variables: Iterable[MetVariable], inplace: bool = False
1184
+ ) -> Self | None:
1185
+ """Standardize variable names.
1186
+
1187
+ .. versionchanged:: 0.54.7
1188
+
1189
+ By default, this method returns a new :class:`MetDataset` instead
1190
+ of renaming in place. To retain the old behavior, set ``inplace=True``.
1183
1191
 
1184
1192
  Parameters
1185
1193
  ----------
1186
1194
  variables : Iterable[MetVariable]
1187
1195
  Data source variables
1196
+ inplace : bool, optional
1197
+ If True, rename variables in place. Otherwise, return a new
1198
+ :class:`MetDataset` with renamed variables.
1188
1199
 
1189
1200
  See Also
1190
1201
  --------
1191
1202
  :func:`standardize_variables`
1192
1203
  """
1193
- standardize_variables(self, variables)
1204
+ data_renamed = standardize_variables(self.data, variables)
1205
+
1206
+ if inplace:
1207
+ self.data = data_renamed
1208
+ return None
1209
+
1210
+ return type(self)._from_fastpath(data_renamed, cachestore=self.cachestore)
1194
1211
 
1195
1212
  @classmethod
1196
1213
  def from_coords(
@@ -2642,7 +2659,7 @@ def downselect(data: XArrayType, bbox: tuple[float, ...]) -> XArrayType:
2642
2659
  return data.where(cond, drop=True)
2643
2660
 
2644
2661
 
2645
- def standardize_variables(ds: DatasetType, variables: Iterable[MetVariable]) -> DatasetType:
2662
+ def standardize_variables(ds: xr.Dataset, variables: Iterable[MetVariable]) -> xr.Dataset:
2646
2663
  """Rename all variables in dataset from short name to standard name.
2647
2664
 
2648
2665
  This function does not change any variables in ``ds`` that are not found in ``variables``.
@@ -2652,8 +2669,7 @@ def standardize_variables(ds: DatasetType, variables: Iterable[MetVariable]) ->
2652
2669
  Parameters
2653
2670
  ----------
2654
2671
  ds : DatasetType
2655
- An :class:`xr.Dataset` or :class:`MetDataset`. When a :class:`MetDataset` is
2656
- passed, the underlying :class:`xr.Dataset` is modified in place.
2672
+ An :class:`xr.Dataset`.
2657
2673
  variables : Iterable[MetVariable]
2658
2674
  Data source variables
2659
2675
 
@@ -2662,14 +2678,6 @@ def standardize_variables(ds: DatasetType, variables: Iterable[MetVariable]) ->
2662
2678
  DatasetType
2663
2679
  Dataset with variables renamed to standard names
2664
2680
  """
2665
- if isinstance(ds, xr.Dataset):
2666
- return _standardize_variables(ds, variables)
2667
-
2668
- ds.data = _standardize_variables(ds.data, variables)
2669
- return ds
2670
-
2671
-
2672
- def _standardize_variables(ds: xr.Dataset, variables: Iterable[MetVariable]) -> xr.Dataset:
2673
2681
  variables_dict: dict[Hashable, str] = {v.short_name: v.standard_name for v in variables}
2674
2682
  name_dict = {var: variables_dict[var] for var in ds.data_vars if var in variables_dict}
2675
2683
  return ds.rename(name_dict)
@@ -282,6 +282,35 @@ VerticalVelocity = MetVariable(
282
282
  ),
283
283
  )
284
284
 
285
+ MassFractionOfCloudLiquidWaterInAir = MetVariable(
286
+ short_name="clw",
287
+ standard_name="mass_fraction_of_cloud_liquid_water_in_air",
288
+ long_name="Mass fraction of cloud liquid water in air",
289
+ units="kg kg**-1",
290
+ level_type="isobaricInhPa",
291
+ amip="clw",
292
+ description=("The mass fraction of cloud liquid water in moist air."),
293
+ )
294
+
295
+ MassFractionOfCloudIceInAir = MetVariable(
296
+ short_name="cli",
297
+ standard_name="mass_fraction_of_cloud_ice_in_air",
298
+ long_name="Mass fraction of cloud ice in air",
299
+ units="kg kg**-1",
300
+ level_type="isobaricInhPa",
301
+ amip="cli",
302
+ description=("The mass fraction of cloud ice in moist air."),
303
+ )
304
+
305
+ CloudAreaFractionInAtmosphereLayer = MetVariable(
306
+ short_name="cl",
307
+ standard_name="cloud_area_fraction_in_atmosphere_layer",
308
+ long_name="Cloud area fraction in atmosphere layer",
309
+ units="[0 - 1]",
310
+ level_type="isobaricInhPa",
311
+ description=("The fraction of the horizontal area of a grid cell that contains cloud."),
312
+ )
313
+
285
314
 
286
315
  # ----
287
316
  # Single level variables
@@ -305,3 +334,54 @@ SurfacePressure = MetVariable(
305
334
  "Earth's surface represented at a fixed point."
306
335
  ),
307
336
  )
337
+
338
+ TOANetDownwardShortwaveFlux = MetVariable(
339
+ short_name="rst",
340
+ standard_name="toa_net_downward_shortwave_flux",
341
+ long_name="TOA net downward shortwave flux",
342
+ units="W m**-2",
343
+ level_type="nominalTop",
344
+ amip="rst",
345
+ description=(
346
+ '"shortwave" means shortwave radiation. "toa" means top of atmosphere. '
347
+ '"Downward" indicates a vector component which is positive when directed '
348
+ "downward (negative upward). Net downward radiation is the difference "
349
+ "between radiation from above (downwelling) and radiation from below (upwelling). "
350
+ "In accordance with common usage in geophysical disciplines, "
351
+ '"flux" implies per unit area, called "flux density" in physics.'
352
+ ),
353
+ )
354
+
355
+ TOAOutgoingLongwaveFlux = MetVariable(
356
+ short_name="rlut",
357
+ standard_name="toa_outgoing_longwave_flux",
358
+ long_name="TOA outgoing longwave_flux",
359
+ units="W m**-2",
360
+ level_type="nominalTop",
361
+ amip="rlut",
362
+ description=(
363
+ '"longwave" means longwave radiation. "toa" means top of atmosphere. '
364
+ "The TOA outgoing longwave flux is the upwelling thermal radiative flux, "
365
+ 'often called the "outgoing longwave radiation" or "OLR". '
366
+ "In accordance with common usage in geophysical disciplines, "
367
+ '"flux" implies per unit area, called "flux density" in physics.'
368
+ ),
369
+ )
370
+
371
+ PRESSURE_LEVEL_VARIABLES = [
372
+ AirTemperature,
373
+ SpecificHumidity,
374
+ RelativeHumidity,
375
+ Geopotential,
376
+ GeopotentialHeight,
377
+ EastwardWind,
378
+ NorthwardWind,
379
+ VerticalVelocity,
380
+ MassFractionOfCloudLiquidWaterInAir,
381
+ MassFractionOfCloudIceInAir,
382
+ CloudAreaFractionInAtmosphereLayer,
383
+ ]
384
+
385
+ SINGLE_LEVEL_VARIABLES = [SurfacePressure, TOANetDownwardShortwaveFlux, TOAOutgoingLongwaveFlux]
386
+
387
+ MET_VARIABLES = PRESSURE_LEVEL_VARIABLES + SINGLE_LEVEL_VARIABLES
@@ -22,8 +22,10 @@ import xarray as xr
22
22
  from pycontrails.core.fleet import Fleet
23
23
  from pycontrails.core.flight import Flight
24
24
  from pycontrails.core.met import MetDataArray, MetDataset, MetVariable, originates_from_ecmwf
25
- from pycontrails.core.met_var import SpecificHumidity
25
+ from pycontrails.core.met_var import MET_VARIABLES, SpecificHumidity
26
26
  from pycontrails.core.vector import GeoVectorDataset
27
+ from pycontrails.datalib.ecmwf import ECMWF_VARIABLES
28
+ from pycontrails.datalib.gfs import GFS_VARIABLES
27
29
  from pycontrails.utils.json import NumpyEncoder
28
30
  from pycontrails.utils.types import type_guard
29
31
 
@@ -179,8 +181,10 @@ class Model(ABC):
179
181
 
180
182
  #: Required meteorology pressure level variables.
181
183
  #: Each element in the list is a :class:`MetVariable` or a ``tuple[MetVariable]``.
182
- #: If element is a ``tuple[MetVariable]``, the variable depends on the data source.
183
- #: Only one variable in the tuple is required.
184
+ #: If element is a ``tuple[MetVariable]``, the variable depends on the data source
185
+ #: and the tuple must include entries for a model-agnostic variable,
186
+ #: an ECMWF-specific variable, and a GFS-specific variable.
187
+ #: Only one of the three variable in the tuple is required for model evaluation.
184
188
  met_variables: tuple[MetVariable | tuple[MetVariable, ...], ...]
185
189
 
186
190
  #: Set of required parameters if processing already complete on ``met`` input.
@@ -276,6 +280,42 @@ class Model(ABC):
276
280
 
277
281
  return hashlib.sha1(bytes(_hash, "utf-8")).hexdigest()
278
282
 
283
+ @classmethod
284
+ def generic_met_variables(cls) -> tuple[MetVariable, ...]:
285
+ """Return a model-agnostic list of required meteorology variables.
286
+
287
+ Returns
288
+ -------
289
+ tuple[MetVariable]
290
+ List of model-agnostic variants of required variables
291
+ """
292
+ available = set(MET_VARIABLES)
293
+ return tuple(_find_match(required, available) for required in cls.met_variables)
294
+
295
+ @classmethod
296
+ def ecmwf_met_variables(cls) -> tuple[MetVariable, ...]:
297
+ """Return an ECMWF-specific list of required meteorology variables.
298
+
299
+ Returns
300
+ -------
301
+ tuple[MetVariable]
302
+ List of ECMWF-specific variants of required variables
303
+ """
304
+ available = set(ECMWF_VARIABLES)
305
+ return tuple(_find_match(required, available) for required in cls.met_variables)
306
+
307
+ @classmethod
308
+ def gfs_met_variables(cls) -> tuple[MetVariable, ...]:
309
+ """Return a GFS-specific list of required meteorology variables.
310
+
311
+ Returns
312
+ -------
313
+ tuple[MetVariable]
314
+ List of GFS-specific variants of required variables
315
+ """
316
+ available = set(GFS_VARIABLES)
317
+ return tuple(_find_match(required, available) for required in cls.met_variables)
318
+
279
319
  def _verify_met(self) -> None:
280
320
  """Verify integrity of :attr:`met`.
281
321
 
@@ -805,6 +845,42 @@ def _interp_grid_to_grid(
805
845
  raise NotImplementedError(msg)
806
846
 
807
847
 
848
+ def _find_match(
849
+ required: MetVariable | Sequence[MetVariable], available: set[MetVariable]
850
+ ) -> MetVariable:
851
+ """Find match for required met variable in list of data-source-specific met variables.
852
+
853
+ Parameters
854
+ ----------
855
+ required : MetVariable | Sequence[MetVariable]
856
+ Required met variable
857
+
858
+ available : Sequence[MetVariable]
859
+ Collection of data-source-specific met variables
860
+
861
+ Returns
862
+ -------
863
+ MetVariable
864
+ Match for required met variable in collection of data-source-specific met variables
865
+
866
+ Raises
867
+ ------
868
+ KeyError
869
+ Raised if not match is found
870
+ """
871
+ if isinstance(required, MetVariable):
872
+ return required
873
+
874
+ for var in required:
875
+ if var in available:
876
+ return var
877
+
878
+ required_keys = [v.standard_name for v in required]
879
+ available_keys = [v.standard_name for v in available]
880
+ msg = f"None of {required_keys} match variable in {available_keys}"
881
+ raise KeyError(msg)
882
+
883
+
808
884
  def _raise_missing_met_var(var: MetVariable | Sequence[MetVariable]) -> NoReturn:
809
885
  """Raise KeyError on missing met variable.
810
886
 
@@ -986,6 +1062,7 @@ def _prepare_q(
986
1062
  return _prepare_q_cubic_spline(da, level)
987
1063
 
988
1064
  raise_invalid_q_method_error(q_method)
1065
+ return None
989
1066
 
990
1067
 
991
1068
  def _prepare_q_log_q_log_p(