pycontrails 0.42.0__cp311-cp311-win_amd64.whl → 0.42.2__cp311-cp311-win_amd64.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 +2 -2
- pycontrails/core/cache.py +4 -6
- pycontrails/core/datalib.py +5 -2
- pycontrails/core/fleet.py +59 -7
- pycontrails/core/flight.py +175 -49
- pycontrails/core/flightplan.py +238 -0
- pycontrails/core/interpolation.py +11 -15
- pycontrails/core/met.py +5 -5
- pycontrails/core/models.py +4 -0
- pycontrails/core/rgi_cython.cp311-win_amd64.pyd +0 -0
- pycontrails/core/vector.py +17 -12
- pycontrails/datalib/ecmwf/common.py +14 -19
- pycontrails/ext/bada/__init__.py +6 -6
- pycontrails/ext/cirium/__init__.py +2 -2
- pycontrails/models/cocip/cocip.py +37 -39
- pycontrails/models/cocip/cocip_params.py +37 -30
- pycontrails/models/cocip/cocip_uncertainty.py +47 -58
- pycontrails/models/cocip/radiative_forcing.py +220 -193
- pycontrails/models/cocip/wake_vortex.py +96 -91
- pycontrails/models/humidity_scaling.py +265 -8
- pycontrails/models/issr.py +1 -1
- pycontrails/models/quantiles/era5_ensemble_quantiles.npy +0 -0
- pycontrails/models/quantiles/iagos_quantiles.npy +0 -0
- pycontrails/models/sac.py +2 -0
- pycontrails/physics/geo.py +2 -1
- pycontrails/utils/json.py +3 -3
- {pycontrails-0.42.0.dist-info → pycontrails-0.42.2.dist-info}/METADATA +4 -7
- {pycontrails-0.42.0.dist-info → pycontrails-0.42.2.dist-info}/RECORD +32 -29
- {pycontrails-0.42.0.dist-info → pycontrails-0.42.2.dist-info}/LICENSE +0 -0
- {pycontrails-0.42.0.dist-info → pycontrails-0.42.2.dist-info}/NOTICE +0 -0
- {pycontrails-0.42.0.dist-info → pycontrails-0.42.2.dist-info}/WHEEL +0 -0
- {pycontrails-0.42.0.dist-info → pycontrails-0.42.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""ATC Flight Plan Parser."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def to_atc_plan(plan: dict[str, Any]) -> str:
|
|
8
|
+
"""Write dictionary from :func:`parse_atc_plan` as ATC flight plan string.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
plan: dict[str, Any]
|
|
13
|
+
Dictionary representation of ATC flight plan returned from :func:`parse_atc_plan`.
|
|
14
|
+
|
|
15
|
+
Returns
|
|
16
|
+
-------
|
|
17
|
+
str
|
|
18
|
+
ATC flight plan string conforming to ICAO Doc 4444-ATM/501
|
|
19
|
+
|
|
20
|
+
See Also
|
|
21
|
+
--------
|
|
22
|
+
:func:`parse_atc_plan`
|
|
23
|
+
"""
|
|
24
|
+
ret = f'(FPL-{plan["callsign"]}-{plan["flight_rules"]}'
|
|
25
|
+
ret += f'{plan["type_of_flight"]}\n'
|
|
26
|
+
ret += "-"
|
|
27
|
+
if "number_aircraft" in plan and plan["number_aircraft"] <= 10:
|
|
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'
|
|
34
|
+
if "destination_icao" in plan and "duration" in plan:
|
|
35
|
+
ret += f'-{plan["destination_icao"]}{plan["duration"]}'
|
|
36
|
+
if "alt_icao" in plan:
|
|
37
|
+
ret += f' {plan["alt_icao"]}'
|
|
38
|
+
if "second_alt_icao" in plan:
|
|
39
|
+
ret += f' {plan["second_alt_icao"]}'
|
|
40
|
+
ret += "\n"
|
|
41
|
+
ret += f'-{plan["other_info"]})\n'
|
|
42
|
+
if "supplementary_info" in plan:
|
|
43
|
+
ret += " ".join([f"{i[0]}/{i[1]}" for i in plan["supplementary_info"].items()])
|
|
44
|
+
|
|
45
|
+
if ret[-1] == "\n":
|
|
46
|
+
ret = ret[:-1]
|
|
47
|
+
|
|
48
|
+
return ret
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_atc_plan(atc_plan: str) -> dict[str, str]:
|
|
52
|
+
"""Parse an ATC flight plan string into a dictionary.
|
|
53
|
+
|
|
54
|
+
The route string is not converted to lat/lon in this process.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
atc_plan : str
|
|
59
|
+
An ATC flight plan string conforming to ICAO Doc 4444-ATM/501 (Appendix 2)
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
dict[str, str]
|
|
64
|
+
A dictionary consisting of parsed components of the ATC flight plan.
|
|
65
|
+
A full ATC plan will contain the keys:
|
|
66
|
+
|
|
67
|
+
- ``callsign``: ICAO flight callsign
|
|
68
|
+
- ``flight_rules``: Flight rules ("I", "V", "Y", "Z")
|
|
69
|
+
- ``type_of_flight``: Type of flight ("S", "N", "G", "M", "X")
|
|
70
|
+
- ``number_aircraft``: The number of aircraft, if more than one
|
|
71
|
+
- ``type_of_aircraft``: ICAO aircraft type
|
|
72
|
+
- ``wake_category``: Wake turbulence category
|
|
73
|
+
- ``equipment``: Radiocommunication, navigation and approach aid equipment and capabilities
|
|
74
|
+
- ``transponder``: Surveillance equipment and capabilities
|
|
75
|
+
- ``departure_icao``: ICAO departure airport
|
|
76
|
+
- ``time``: Estimated off-block (departure) time (UTC)
|
|
77
|
+
- ``speed_type``: Speed units ("K": km / hr, "N": knots)
|
|
78
|
+
- ``speed``: Cruise true airspeed in ``speed_type`` units
|
|
79
|
+
- ``level_type``: Level units ("F", "S", "A", "M")
|
|
80
|
+
- ``level``: Cruise level
|
|
81
|
+
- ``route``: Route string
|
|
82
|
+
- ``destination_icao``: ICAO destination airport
|
|
83
|
+
- ``duration``: The total estimated elapsed time for the flight plan
|
|
84
|
+
- ``alt_icao``: ICAO alternate destination airport
|
|
85
|
+
- ``second_alt_icao``: ICAO second alternate destination airport
|
|
86
|
+
- ``other_info``: Other information
|
|
87
|
+
- ``supplementary_info``: Supplementary information
|
|
88
|
+
|
|
89
|
+
References
|
|
90
|
+
----------
|
|
91
|
+
- https://applications.icao.int/tools/ATMiKIT/story_content/external_files/story_content/external_files/DOC%204444_PANS%20ATM_en.pdf
|
|
92
|
+
|
|
93
|
+
See Also
|
|
94
|
+
--------
|
|
95
|
+
:func:`to_atc_plan`
|
|
96
|
+
""" # noqa: E501
|
|
97
|
+
atc_plan = atc_plan.replace("\r", "")
|
|
98
|
+
atc_plan = atc_plan.replace("\n", "")
|
|
99
|
+
atc_plan = atc_plan.upper()
|
|
100
|
+
atc_plan = atc_plan.strip()
|
|
101
|
+
|
|
102
|
+
if len(atc_plan) == 0:
|
|
103
|
+
raise ValueError("Empty or invalid flight plan")
|
|
104
|
+
|
|
105
|
+
atc_plan = atc_plan.replace("(FPL", "")
|
|
106
|
+
atc_plan = atc_plan.replace(")", "")
|
|
107
|
+
atc_plan = atc_plan.replace("--", "-")
|
|
108
|
+
|
|
109
|
+
basic = atc_plan.split("-")
|
|
110
|
+
|
|
111
|
+
flightplan: dict[str, Any] = {}
|
|
112
|
+
|
|
113
|
+
# Callsign
|
|
114
|
+
if len(basic) > 1:
|
|
115
|
+
flightplan["callsign"] = basic[1]
|
|
116
|
+
|
|
117
|
+
# Flight Rules
|
|
118
|
+
if len(basic) > 2:
|
|
119
|
+
flightplan["flight_rules"] = basic[2][0]
|
|
120
|
+
flightplan["type_of_flight"] = basic[2][1]
|
|
121
|
+
|
|
122
|
+
# Aircraft
|
|
123
|
+
if len(basic) > 3:
|
|
124
|
+
aircraft = basic[3].split("/")
|
|
125
|
+
matches = re.match("(\d{1})(\S{3,4})", aircraft[0])
|
|
126
|
+
if matches:
|
|
127
|
+
groups = matches.groups()
|
|
128
|
+
else:
|
|
129
|
+
groups = ()
|
|
130
|
+
|
|
131
|
+
if matches and len(groups) > 2:
|
|
132
|
+
flightplan["number_aircraft"] = groups[1]
|
|
133
|
+
flightplan["type_of_aircraft"] = groups[2]
|
|
134
|
+
else:
|
|
135
|
+
flightplan["type_of_aircraft"] = aircraft[0]
|
|
136
|
+
|
|
137
|
+
if len(aircraft) > 1:
|
|
138
|
+
flightplan["wake_category"] = aircraft[1]
|
|
139
|
+
|
|
140
|
+
# Equipment
|
|
141
|
+
if len(basic) > 4:
|
|
142
|
+
equip = basic[4].split("/")
|
|
143
|
+
flightplan["equipment"] = equip[0]
|
|
144
|
+
if len(equip) > 1:
|
|
145
|
+
flightplan["transponder"] = equip[1]
|
|
146
|
+
|
|
147
|
+
# Dep. airport info
|
|
148
|
+
if len(basic) > 5:
|
|
149
|
+
matches = re.match("(\D*)(\d*)", basic[5])
|
|
150
|
+
if matches:
|
|
151
|
+
groups = matches.groups()
|
|
152
|
+
else:
|
|
153
|
+
groups = ()
|
|
154
|
+
|
|
155
|
+
if len(groups) > 0:
|
|
156
|
+
flightplan["departure_icao"] = groups[0]
|
|
157
|
+
if len(groups) > 1:
|
|
158
|
+
flightplan["time"] = groups[1]
|
|
159
|
+
|
|
160
|
+
# Speed and route info
|
|
161
|
+
if len(basic) > 6:
|
|
162
|
+
matches = re.match("(\D*)(\d*)(\D*)(\d*)", basic[6])
|
|
163
|
+
if matches:
|
|
164
|
+
groups = matches.groups()
|
|
165
|
+
else:
|
|
166
|
+
groups = ()
|
|
167
|
+
|
|
168
|
+
# match speed and level
|
|
169
|
+
if len(groups) > 0:
|
|
170
|
+
flightplan["speed_type"] = groups[0]
|
|
171
|
+
if len(groups) > 1:
|
|
172
|
+
flightplan["speed"] = groups[1]
|
|
173
|
+
if len(groups) > 2:
|
|
174
|
+
flightplan["level_type"] = groups[2]
|
|
175
|
+
if len(groups) > 3:
|
|
176
|
+
flightplan["level"] = groups[3]
|
|
177
|
+
|
|
178
|
+
flightplan["route"] = basic[6][len("".join(groups)) :].strip()
|
|
179
|
+
else:
|
|
180
|
+
flightplan["route"] = basic[6].strip()
|
|
181
|
+
|
|
182
|
+
# Dest. airport info
|
|
183
|
+
if len(basic) > 7:
|
|
184
|
+
matches = re.match("(\D{4})(\d{4})", basic[7])
|
|
185
|
+
if matches:
|
|
186
|
+
groups = matches.groups()
|
|
187
|
+
else:
|
|
188
|
+
groups = ()
|
|
189
|
+
|
|
190
|
+
if len(groups) > 0:
|
|
191
|
+
flightplan["destination_icao"] = groups[0]
|
|
192
|
+
if len(groups) > 1:
|
|
193
|
+
flightplan["duration"] = groups[1]
|
|
194
|
+
|
|
195
|
+
matches = re.match("(\D{4})(\d{4})(\s{1})(\D{4})", basic[7])
|
|
196
|
+
if matches:
|
|
197
|
+
groups = matches.groups()
|
|
198
|
+
else:
|
|
199
|
+
groups = ()
|
|
200
|
+
|
|
201
|
+
if len(groups) > 3:
|
|
202
|
+
flightplan["alt_icao"] = groups[3]
|
|
203
|
+
|
|
204
|
+
matches = re.match("(\D{4})(\d{4})(\s{1})(\D{4})(\s{1})(\D{4})", basic[7])
|
|
205
|
+
if matches:
|
|
206
|
+
groups = matches.groups()
|
|
207
|
+
else:
|
|
208
|
+
groups = ()
|
|
209
|
+
|
|
210
|
+
if len(groups) > 5:
|
|
211
|
+
flightplan["second_alt_icao"] = groups[5]
|
|
212
|
+
|
|
213
|
+
# Other info
|
|
214
|
+
if len(basic) > 8:
|
|
215
|
+
flightplan["other_info"] = basic[8]
|
|
216
|
+
|
|
217
|
+
# Supl. Info
|
|
218
|
+
if len(basic) > 9:
|
|
219
|
+
sup_match = re.findall("(\D{1}[\/]{1})", basic[9])
|
|
220
|
+
if len(sup_match) > 0:
|
|
221
|
+
suplInfo = {}
|
|
222
|
+
for i in range(len(sup_match) - 1):
|
|
223
|
+
this_key = sup_match[i]
|
|
224
|
+
this_idx = basic[9].find(this_key)
|
|
225
|
+
|
|
226
|
+
next_key = sup_match[i + 1]
|
|
227
|
+
next_idx = basic[9].find(next_key)
|
|
228
|
+
|
|
229
|
+
val = basic[9][this_idx + 2 : next_idx - 1]
|
|
230
|
+
suplInfo[this_key[0]] = val
|
|
231
|
+
|
|
232
|
+
last_key = sup_match[-1]
|
|
233
|
+
last_idx = basic[9].find(last_key)
|
|
234
|
+
suplInfo[last_key[0]] = basic[9][last_idx + 2 :]
|
|
235
|
+
|
|
236
|
+
flightplan["supplementary_info"] = suplInfo
|
|
237
|
+
|
|
238
|
+
return flightplan
|
|
@@ -74,7 +74,7 @@ class PycontrailsRegularGridInterpolator(scipy.interpolate.RegularGridInterpolat
|
|
|
74
74
|
self.bounds_error = bounds_error
|
|
75
75
|
self.fill_value = fill_value
|
|
76
76
|
|
|
77
|
-
def _prepare_xi_simple(self, xi: npt.NDArray[np.float64]) -> npt.NDArray[np.bool_]
|
|
77
|
+
def _prepare_xi_simple(self, xi: npt.NDArray[np.float64]) -> npt.NDArray[np.bool_]:
|
|
78
78
|
"""Run looser version of :meth:`_prepare_xi`.
|
|
79
79
|
|
|
80
80
|
Parameters
|
|
@@ -84,12 +84,9 @@ class PycontrailsRegularGridInterpolator(scipy.interpolate.RegularGridInterpolat
|
|
|
84
84
|
|
|
85
85
|
Returns
|
|
86
86
|
-------
|
|
87
|
-
npt.NDArray[np.bool_]
|
|
87
|
+
npt.NDArray[np.bool_]
|
|
88
88
|
A 1-dimensional Boolean array indicating which points are out of bounds.
|
|
89
|
-
If ``bounds_error`` is ``True``, this will be ``
|
|
90
|
-
no points are out of bounds. (This is the same convention as
|
|
91
|
-
:meth:`scipy.interpolate.RegularGridInterpolator._prepare_xi`). If
|
|
92
|
-
every point is in bounds, this is set to ``None``.
|
|
89
|
+
If ``bounds_error`` is ``True``, this will be all ``False``.
|
|
93
90
|
"""
|
|
94
91
|
|
|
95
92
|
if self.bounds_error:
|
|
@@ -99,12 +96,9 @@ class PycontrailsRegularGridInterpolator(scipy.interpolate.RegularGridInterpolat
|
|
|
99
96
|
if not (np.all(p >= g0) and np.all(p <= g1)):
|
|
100
97
|
raise ValueError(f"One of the requested xi is out of bounds in dimension {i}")
|
|
101
98
|
|
|
102
|
-
return
|
|
99
|
+
return np.zeros(xi.shape[0], dtype=bool)
|
|
103
100
|
|
|
104
|
-
|
|
105
|
-
if not np.any(out_of_bounds):
|
|
106
|
-
return None
|
|
107
|
-
return out_of_bounds
|
|
101
|
+
return self._find_out_of_bounds(xi.T)
|
|
108
102
|
|
|
109
103
|
def __call__(
|
|
110
104
|
self, xi: npt.NDArray[np.float64], method: str | None = None
|
|
@@ -137,7 +131,9 @@ class PycontrailsRegularGridInterpolator(scipy.interpolate.RegularGridInterpolat
|
|
|
137
131
|
return self._set_out_of_bounds(out, out_of_bounds)
|
|
138
132
|
|
|
139
133
|
def _set_out_of_bounds(
|
|
140
|
-
self,
|
|
134
|
+
self,
|
|
135
|
+
out: npt.NDArray[np.float_],
|
|
136
|
+
out_of_bounds: npt.NDArray[np.bool_],
|
|
141
137
|
) -> npt.NDArray[np.float_]:
|
|
142
138
|
"""Set out-of-bounds values to the fill value.
|
|
143
139
|
|
|
@@ -145,7 +141,7 @@ class PycontrailsRegularGridInterpolator(scipy.interpolate.RegularGridInterpolat
|
|
|
145
141
|
----------
|
|
146
142
|
out : npt.NDArray[np.float_]
|
|
147
143
|
Values from interpolation. This is modified in-place.
|
|
148
|
-
out_of_bounds : npt.NDArray[np.bool_]
|
|
144
|
+
out_of_bounds : npt.NDArray[np.bool_]
|
|
149
145
|
A 1-dimensional Boolean array indicating which points are out of bounds.
|
|
150
146
|
|
|
151
147
|
Returns
|
|
@@ -153,7 +149,7 @@ class PycontrailsRegularGridInterpolator(scipy.interpolate.RegularGridInterpolat
|
|
|
153
149
|
out : npt.NDArray[np.float_]
|
|
154
150
|
A reference to the ``out`` array.
|
|
155
151
|
"""
|
|
156
|
-
if
|
|
152
|
+
if self.fill_value is not None and np.any(out_of_bounds):
|
|
157
153
|
out[out_of_bounds] = self.fill_value
|
|
158
154
|
|
|
159
155
|
return out
|
|
@@ -557,7 +553,7 @@ class RGIArtifacts:
|
|
|
557
553
|
|
|
558
554
|
xi_indices: npt.NDArray[np.int64]
|
|
559
555
|
norm_distances: npt.NDArray[np.float64]
|
|
560
|
-
out_of_bounds: npt.NDArray[np.bool_]
|
|
556
|
+
out_of_bounds: npt.NDArray[np.bool_]
|
|
561
557
|
|
|
562
558
|
|
|
563
559
|
# ------------------------------------------------------------------------------
|
pycontrails/core/met.py
CHANGED
|
@@ -416,7 +416,7 @@ class MetBase(ABC, Generic[XArrayType]):
|
|
|
416
416
|
>>> variables = "air_temperature", "specific_humidity"
|
|
417
417
|
>>> levels = [200, 300]
|
|
418
418
|
>>> era5 = ERA5(times, variables, levels)
|
|
419
|
-
>>> mds = era5.open_metdataset(
|
|
419
|
+
>>> mds = era5.open_metdataset()
|
|
420
420
|
>>> mds.variables["level"].values # faster access than mds.data["level"]
|
|
421
421
|
array([200., 300.])
|
|
422
422
|
|
|
@@ -606,7 +606,7 @@ class MetDataset(MetBase):
|
|
|
606
606
|
>>> era5 = ERA5(time, variables, pressure_levels)
|
|
607
607
|
|
|
608
608
|
>>> # Open directly as `MetDataset`
|
|
609
|
-
>>> met = era5.open_metdataset(
|
|
609
|
+
>>> met = era5.open_metdataset()
|
|
610
610
|
>>> # Use `data` attribute to access `xarray` object
|
|
611
611
|
>>> assert isinstance(met.data, xr.Dataset)
|
|
612
612
|
|
|
@@ -992,7 +992,7 @@ class MetDataset(MetBase):
|
|
|
992
992
|
>>> variables = ["air_temperature", "specific_humidity"]
|
|
993
993
|
>>> levels = [250, 200]
|
|
994
994
|
>>> era5 = ERA5(time=times, variables=variables, pressure_levels=levels)
|
|
995
|
-
>>> met = era5.open_metdataset(
|
|
995
|
+
>>> met = era5.open_metdataset()
|
|
996
996
|
>>> met.to_vector(transfer_attrs=False)
|
|
997
997
|
GeoVectorDataset [6 keys x 4152960 length, 1 attributes]
|
|
998
998
|
Keys: longitude, latitude, level, time, air_temperature, ..., specific_humidity
|
|
@@ -1507,7 +1507,7 @@ class MetDataArray(MetBase):
|
|
|
1507
1507
|
>>> variables = "air_temperature"
|
|
1508
1508
|
>>> levels = [200, 250, 300]
|
|
1509
1509
|
>>> era5 = ERA5(times, variables, levels)
|
|
1510
|
-
>>> met = era5.open_metdataset(
|
|
1510
|
+
>>> met = era5.open_metdataset()
|
|
1511
1511
|
>>> mda = met["air_temperature"]
|
|
1512
1512
|
|
|
1513
1513
|
>>> # Interpolation at a grid point agrees with value
|
|
@@ -1779,7 +1779,7 @@ class MetDataArray(MetBase):
|
|
|
1779
1779
|
>>> from pprint import pprint
|
|
1780
1780
|
>>> from pycontrails.datalib.ecmwf import ERA5
|
|
1781
1781
|
>>> era5 = ERA5("2022-03-01", variables="air_temperature", pressure_levels=250)
|
|
1782
|
-
>>> mda = era5.open_metdataset(
|
|
1782
|
+
>>> mda = era5.open_metdataset()["air_temperature"]
|
|
1783
1783
|
>>> mda.shape
|
|
1784
1784
|
(1440, 721, 1, 1)
|
|
1785
1785
|
|
pycontrails/core/models.py
CHANGED
|
@@ -566,6 +566,10 @@ class Model(ABC):
|
|
|
566
566
|
try:
|
|
567
567
|
# This case is when self.source is a subgrid of self.met
|
|
568
568
|
# The call to .sel will raise a KeyError if this is not the case
|
|
569
|
+
|
|
570
|
+
# XXX: Sometimes this hangs when using dask!
|
|
571
|
+
# This issue is somewhat similar to
|
|
572
|
+
# https://github.com/pydata/xarray/issues/4406
|
|
569
573
|
self.source[met_key] = da.sel(self.source.coords)
|
|
570
574
|
|
|
571
575
|
except KeyError:
|
|
Binary file
|
pycontrails/core/vector.py
CHANGED
|
@@ -287,15 +287,17 @@ class VectorDataset:
|
|
|
287
287
|
# Take extra caution with a time column
|
|
288
288
|
|
|
289
289
|
if "time" in data:
|
|
290
|
-
|
|
290
|
+
time = data["time"]
|
|
291
|
+
|
|
292
|
+
if not hasattr(time, "dt"):
|
|
291
293
|
# If the time column is a string, we try to convert it to a datetime
|
|
292
294
|
# If it fails (for example, a unix integer time), we raise an error
|
|
293
295
|
# and let the user figure it out.
|
|
294
296
|
try:
|
|
295
|
-
|
|
297
|
+
time = pd.to_datetime(time)
|
|
296
298
|
except ValueError:
|
|
297
299
|
raise ValueError(
|
|
298
|
-
"
|
|
300
|
+
"The 'time' field must hold datetime-like values. "
|
|
299
301
|
'Try data["time"] = pd.to_datetime(data["time"], unit=...) '
|
|
300
302
|
"with the appropriate unit."
|
|
301
303
|
)
|
|
@@ -305,13 +307,19 @@ class VectorDataset:
|
|
|
305
307
|
# we raise an error in this case. Timezone issues are complicated,
|
|
306
308
|
# and so it is better for the user to handle them rather than try
|
|
307
309
|
# to address them here.
|
|
308
|
-
if
|
|
310
|
+
if time.dt.tz is not None:
|
|
309
311
|
raise ValueError(
|
|
310
|
-
"
|
|
312
|
+
"The 'time' field must be timezone naive. "
|
|
311
313
|
"This can be achieved with: "
|
|
312
314
|
'data["time"] = data["time"].dt.tz_localize(None)'
|
|
313
315
|
)
|
|
314
|
-
|
|
316
|
+
|
|
317
|
+
data = {col: ser.to_numpy(copy=copy) for col, ser in data.items() if col != "time"}
|
|
318
|
+
data["time"] = time.to_numpy(copy=copy)
|
|
319
|
+
else:
|
|
320
|
+
data = {col: ser.to_numpy(copy=copy) for col, ser in data.items()}
|
|
321
|
+
|
|
322
|
+
self.data = VectorDataDict(data)
|
|
315
323
|
|
|
316
324
|
elif isinstance(data, VectorDataDict) and not copy:
|
|
317
325
|
self.data = data
|
|
@@ -1465,7 +1473,7 @@ class GeoVectorDataset(VectorDataset):
|
|
|
1465
1473
|
>>> variables = ["air_temperature", "specific_humidity"]
|
|
1466
1474
|
>>> levels = [300, 250, 200]
|
|
1467
1475
|
>>> era5 = ERA5(time=times, variables=variables, pressure_levels=levels)
|
|
1468
|
-
>>> met = era5.open_metdataset(
|
|
1476
|
+
>>> met = era5.open_metdataset()
|
|
1469
1477
|
|
|
1470
1478
|
>>> # Example flight
|
|
1471
1479
|
>>> df = pd.DataFrame()
|
|
@@ -1562,9 +1570,7 @@ class GeoVectorDataset(VectorDataset):
|
|
|
1562
1570
|
self["_distances_y"] = distances_y
|
|
1563
1571
|
self["_distances_z"] = distances_z
|
|
1564
1572
|
self["_distances_t"] = distances_t
|
|
1565
|
-
|
|
1566
|
-
if out_of_bounds is not None:
|
|
1567
|
-
self["_out_of_bounds"] = out_of_bounds
|
|
1573
|
+
self["_out_of_bounds"] = out_of_bounds
|
|
1568
1574
|
|
|
1569
1575
|
def _get_indices(self) -> interpolation.RGIArtifacts | None:
|
|
1570
1576
|
"""Get entries from call to :meth:`_put_indices`.
|
|
@@ -1589,14 +1595,13 @@ class GeoVectorDataset(VectorDataset):
|
|
|
1589
1595
|
distances_y = self["_distances_y"]
|
|
1590
1596
|
distances_z = self["_distances_z"]
|
|
1591
1597
|
distances_t = self["_distances_t"]
|
|
1598
|
+
out_of_bounds = self["_out_of_bounds"]
|
|
1592
1599
|
except KeyError:
|
|
1593
1600
|
return None
|
|
1594
1601
|
|
|
1595
1602
|
indices = np.asarray([indices_x, indices_y, indices_z, indices_t])
|
|
1596
1603
|
distances = np.asarray([distances_x, distances_y, distances_z, distances_t])
|
|
1597
1604
|
|
|
1598
|
-
out_of_bounds = self.get("_out_of_bounds", None)
|
|
1599
|
-
|
|
1600
1605
|
return interpolation.RGIArtifacts(indices, distances, out_of_bounds)
|
|
1601
1606
|
|
|
1602
1607
|
def _invalidate_indices(self) -> None:
|
|
@@ -77,21 +77,18 @@ class ECMWFAPI(datalib.MetDataSource):
|
|
|
77
77
|
raise KeyError(f"Input dataset is missing variables {e}")
|
|
78
78
|
|
|
79
79
|
# downselect times
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
if not self.timesteps:
|
|
81
|
+
self.timesteps = ds["time"].values.astype("datetime64[ns]").tolist()
|
|
82
|
+
else:
|
|
83
|
+
try:
|
|
82
84
|
ds = ds.sel(time=self.timesteps)
|
|
83
|
-
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
missing_times = list(set(np_timesteps) - set(ds["time"].values))
|
|
91
|
-
missing_times.sort()
|
|
92
|
-
raise KeyError(
|
|
93
|
-
f"Input dataset is missing time coordinates {[str(t) for t in missing_times]}"
|
|
94
|
-
)
|
|
85
|
+
except KeyError:
|
|
86
|
+
# this snippet shows the missing times for convenience
|
|
87
|
+
np_timesteps = [np.datetime64(t, "ns") for t in self.timesteps]
|
|
88
|
+
missing_times = sorted(set(np_timesteps) - set(ds["time"].values))
|
|
89
|
+
raise KeyError(
|
|
90
|
+
f"Input dataset is missing time coordinates {[str(t) for t in missing_times]}"
|
|
91
|
+
)
|
|
95
92
|
|
|
96
93
|
# downselect pressure level
|
|
97
94
|
# if "level" is not in dims and
|
|
@@ -104,16 +101,12 @@ class ECMWFAPI(datalib.MetDataSource):
|
|
|
104
101
|
ds = ds.sel(level=self.pressure_levels)
|
|
105
102
|
except KeyError:
|
|
106
103
|
# this snippet shows the missing levels for convenience
|
|
107
|
-
missing_levels =
|
|
108
|
-
missing_levels.sort()
|
|
104
|
+
missing_levels = sorted(set(self.pressure_levels) - set(ds["level"].values))
|
|
109
105
|
raise KeyError(f"Input dataset is missing level coordinates {missing_levels}")
|
|
110
106
|
|
|
111
107
|
# harmonize variable names
|
|
112
108
|
ds = met.standardize_variables(ds, self.variables)
|
|
113
109
|
|
|
114
|
-
if "cachestore" not in kwargs:
|
|
115
|
-
kwargs["cachestore"] = self.cachestore
|
|
116
|
-
|
|
117
110
|
# modify values
|
|
118
111
|
|
|
119
112
|
# rescale relative humidity from % -> dimensionless if its in dataset
|
|
@@ -129,4 +122,6 @@ class ECMWFAPI(datalib.MetDataSource):
|
|
|
129
122
|
] = "Relative humidity rescaled to [0 - 1] instead of %"
|
|
130
123
|
|
|
131
124
|
ds.attrs["met_source"] = type(self).__name__
|
|
125
|
+
|
|
126
|
+
kwargs.setdefault("cachestore", self.cachestore)
|
|
132
127
|
return met.MetDataset(ds, **kwargs)
|
pycontrails/ext/bada/__init__.py
CHANGED
|
@@ -20,6 +20,12 @@ try:
|
|
|
20
20
|
BADAParams,
|
|
21
21
|
)
|
|
22
22
|
|
|
23
|
+
except ImportError as e:
|
|
24
|
+
raise ImportError(
|
|
25
|
+
'Failed to import `pycontrails-bada` extension. Install with `pip install "pycontrails-bada'
|
|
26
|
+
' @ git+ssh://git@github.com/contrailcirrus/pycontrails-bada.git"`'
|
|
27
|
+
) from e
|
|
28
|
+
else:
|
|
23
29
|
__all__ = [
|
|
24
30
|
"BADA",
|
|
25
31
|
"BADA3",
|
|
@@ -33,9 +39,3 @@ try:
|
|
|
33
39
|
"bada4",
|
|
34
40
|
"bada_model",
|
|
35
41
|
]
|
|
36
|
-
|
|
37
|
-
except ImportError as e:
|
|
38
|
-
raise ImportError(
|
|
39
|
-
'Failed to import `pycontrails-bada` extension. Install with `pip install "pycontrails-bada'
|
|
40
|
-
' @ git+ssh://git@github.com/contrailcirrus/pycontrails-bada.git"`'
|
|
41
|
-
) from e
|
|
@@ -5,10 +5,10 @@ from __future__ import annotations
|
|
|
5
5
|
try:
|
|
6
6
|
from pycontrails_cirium import Cirium
|
|
7
7
|
|
|
8
|
-
__all__ = ["Cirium"]
|
|
9
|
-
|
|
10
8
|
except ImportError as e:
|
|
11
9
|
raise ImportError(
|
|
12
10
|
"Failed to import `pycontrails-cirium` extension. Install with `pip install"
|
|
13
11
|
' "pycontrails-cirium @ git+ssh://git@github.com/contrailcirrus/pycontrails-cirium.git"`'
|
|
14
12
|
) from e
|
|
13
|
+
else:
|
|
14
|
+
__all__ = ["Cirium"]
|