pycontrails 0.42.0__cp311-cp311-macosx_11_0_arm64.whl → 0.42.2__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 (32) hide show
  1. pycontrails/_version.py +2 -2
  2. pycontrails/core/cache.py +4 -6
  3. pycontrails/core/datalib.py +5 -2
  4. pycontrails/core/fleet.py +59 -7
  5. pycontrails/core/flight.py +175 -49
  6. pycontrails/core/flightplan.py +238 -0
  7. pycontrails/core/interpolation.py +11 -15
  8. pycontrails/core/met.py +5 -5
  9. pycontrails/core/models.py +4 -0
  10. pycontrails/core/rgi_cython.cpython-311-darwin.so +0 -0
  11. pycontrails/core/vector.py +17 -12
  12. pycontrails/datalib/ecmwf/common.py +14 -19
  13. pycontrails/ext/bada/__init__.py +6 -6
  14. pycontrails/ext/cirium/__init__.py +2 -2
  15. pycontrails/models/cocip/cocip.py +37 -39
  16. pycontrails/models/cocip/cocip_params.py +37 -30
  17. pycontrails/models/cocip/cocip_uncertainty.py +47 -58
  18. pycontrails/models/cocip/radiative_forcing.py +220 -193
  19. pycontrails/models/cocip/wake_vortex.py +96 -91
  20. pycontrails/models/humidity_scaling.py +265 -8
  21. pycontrails/models/issr.py +1 -1
  22. pycontrails/models/quantiles/era5_ensemble_quantiles.npy +0 -0
  23. pycontrails/models/quantiles/iagos_quantiles.npy +0 -0
  24. pycontrails/models/sac.py +2 -0
  25. pycontrails/physics/geo.py +2 -1
  26. pycontrails/utils/json.py +3 -3
  27. {pycontrails-0.42.0.dist-info → pycontrails-0.42.2.dist-info}/METADATA +4 -7
  28. {pycontrails-0.42.0.dist-info → pycontrails-0.42.2.dist-info}/RECORD +32 -29
  29. {pycontrails-0.42.0.dist-info → pycontrails-0.42.2.dist-info}/LICENSE +0 -0
  30. {pycontrails-0.42.0.dist-info → pycontrails-0.42.2.dist-info}/NOTICE +0 -0
  31. {pycontrails-0.42.0.dist-info → pycontrails-0.42.2.dist-info}/WHEEL +0 -0
  32. {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_] | None:
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_] | None
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 ``None``, indicating that
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 None
99
+ return np.zeros(xi.shape[0], dtype=bool)
103
100
 
104
- out_of_bounds = self._find_out_of_bounds(xi.T)
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, out: npt.NDArray[np.float_], out_of_bounds: npt.NDArray[np.bool_] | None
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_] | None
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 out_of_bounds is not None and self.fill_value is not None:
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_] | None
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(xr_kwargs=dict(parallel=False))
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(xr_kwargs=dict(parallel=False))
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(xr_kwargs=dict(parallel=False))
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(xr_kwargs=dict(parallel=False))
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(xr_kwargs=dict(parallel=False))["air_temperature"]
1782
+ >>> mda = era5.open_metdataset()["air_temperature"]
1783
1783
  >>> mda.shape
1784
1784
  (1440, 721, 1, 1)
1785
1785
 
@@ -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:
@@ -287,15 +287,17 @@ class VectorDataset:
287
287
  # Take extra caution with a time column
288
288
 
289
289
  if "time" in data:
290
- if not hasattr(data["time"], "dt"):
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
- data["time"] = pd.to_datetime(data["time"])
297
+ time = pd.to_datetime(time)
296
298
  except ValueError:
297
299
  raise ValueError(
298
- "Column `time` must hold datetimelike values. "
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 data["time"].dt.tz is not None:
310
+ if time.dt.tz is not None:
309
311
  raise ValueError(
310
- "Column `time` must be timezone naive. "
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
- self.data = VectorDataDict({col: ser.to_numpy(copy=copy) for col, ser in data.items()})
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(xr_kwargs=dict(parallel=False))
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
- try:
81
- if self.timesteps:
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
- else:
84
- # set timesteps from dataset "time" coordinates
85
- # np.datetime64 doesn't covert to list[datetime] unless its unit is us
86
- self.timesteps = ds["time"].values.astype("datetime64[us]").tolist()
87
- except KeyError:
88
- # this snippet shows the missing times for convenience
89
- np_timesteps = [np.datetime64(t, "ns") for t in self.timesteps]
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 = list(set(self.pressure_levels) - set(ds["level"].values))
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)
@@ -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"]