pycontrails 0.54.5__cp313-cp313-macosx_11_0_arm64.whl → 0.54.7__cp313-cp313-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/__init__.py +1 -1
- pycontrails/_version.py +2 -2
- pycontrails/core/aircraft_performance.py +46 -46
- pycontrails/core/airports.py +7 -5
- pycontrails/core/flight.py +6 -8
- pycontrails/core/flightplan.py +11 -11
- pycontrails/core/met.py +41 -33
- pycontrails/core/met_var.py +80 -0
- pycontrails/core/models.py +80 -3
- pycontrails/core/rgi_cython.cpython-313-darwin.so +0 -0
- pycontrails/core/vector.py +66 -0
- pycontrails/datalib/_met_utils/metsource.py +1 -1
- pycontrails/datalib/ecmwf/era5.py +5 -6
- pycontrails/datalib/ecmwf/era5_model_level.py +4 -5
- pycontrails/datalib/ecmwf/ifs.py +1 -3
- pycontrails/datalib/gfs/gfs.py +1 -3
- pycontrails/datalib/spire/__init__.py +5 -0
- pycontrails/datalib/spire/exceptions.py +62 -0
- pycontrails/datalib/spire/spire.py +606 -0
- pycontrails/models/accf.py +4 -4
- pycontrails/models/cocip/cocip.py +116 -19
- pycontrails/models/cocip/cocip_params.py +10 -1
- pycontrails/models/cocip/output_formats.py +1 -0
- pycontrails/models/cocip/unterstrasser_wake_vortex.py +132 -30
- pycontrails/models/cocipgrid/cocip_grid.py +3 -0
- pycontrails/models/dry_advection.py +51 -19
- pycontrails/models/emissions/black_carbon.py +19 -14
- pycontrails/models/emissions/emissions.py +8 -8
- pycontrails/models/humidity_scaling/humidity_scaling.py +1 -1
- pycontrails/models/pcc.py +1 -2
- pycontrails/models/ps_model/ps_model.py +3 -31
- pycontrails/models/ps_model/ps_operational_limits.py +2 -6
- pycontrails/models/tau_cirrus.py +13 -6
- pycontrails/physics/constants.py +2 -1
- pycontrails/physics/geo.py +3 -3
- {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/METADATA +5 -6
- {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/NOTICE +1 -1
- {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/RECORD +41 -39
- {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/WHEEL +1 -1
- pycontrails/datalib/spire.py +0 -739
- {pycontrails-0.54.5.dist-info → pycontrails-0.54.7.dist-info}/LICENSE +0 -0
- {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
|
@@ -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
|
-
|
|
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
|
-
|
|
516
|
-
|
|
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]
|
pycontrails/core/airports.py
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
continue
|
|
187
|
+
continue
|
|
186
188
|
|
|
187
189
|
return None
|
|
188
190
|
|
pycontrails/core/flight.py
CHANGED
|
@@ -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
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
1986
|
-
|
|
1987
|
-
return result
|
|
1985
|
+
return _sg_filter(result, window_length=kernel_size)
|
|
1988
1986
|
|
|
1989
1987
|
|
|
1990
1988
|
def segment_duration(
|
pycontrails/core/flightplan.py
CHANGED
|
@@ -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
|
|
25
|
-
ret += f
|
|
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
|
|
30
|
-
ret += f
|
|
31
|
-
ret += f
|
|
32
|
-
ret += f
|
|
33
|
-
ret += f
|
|
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
|
|
35
|
+
ret += f"-{plan['destination_icao']}{plan['duration']}"
|
|
36
36
|
if "alt_icao" in plan:
|
|
37
|
-
ret += f
|
|
37
|
+
ret += f" {plan['alt_icao']}"
|
|
38
38
|
if "second_alt_icao" in plan:
|
|
39
|
-
ret += f
|
|
39
|
+
ret += f" {plan['second_alt_icao']}"
|
|
40
40
|
ret += "\n"
|
|
41
|
-
ret += f
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
198
|
-
|
|
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}'.
|
|
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
|
-
|
|
262
|
-
|
|
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
|
-
|
|
1182
|
-
|
|
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:
|
|
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
|
|
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)
|
pycontrails/core/met_var.py
CHANGED
|
@@ -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
|
pycontrails/core/models.py
CHANGED
|
@@ -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
|
-
#:
|
|
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(
|
|
Binary file
|