pycontrails 0.41.0__cp39-cp39-macosx_11_0_arm64.whl → 0.42.2__cp39-cp39-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 (40) hide show
  1. pycontrails/_version.py +2 -2
  2. pycontrails/core/airports.py +228 -0
  3. pycontrails/core/cache.py +4 -6
  4. pycontrails/core/datalib.py +13 -6
  5. pycontrails/core/fleet.py +72 -20
  6. pycontrails/core/flight.py +485 -134
  7. pycontrails/core/flightplan.py +238 -0
  8. pycontrails/core/interpolation.py +11 -15
  9. pycontrails/core/met.py +5 -5
  10. pycontrails/core/models.py +4 -0
  11. pycontrails/core/rgi_cython.cpython-39-darwin.so +0 -0
  12. pycontrails/core/vector.py +80 -63
  13. pycontrails/datalib/__init__.py +1 -1
  14. pycontrails/datalib/ecmwf/common.py +14 -19
  15. pycontrails/datalib/spire/__init__.py +19 -0
  16. pycontrails/datalib/spire/spire.py +739 -0
  17. pycontrails/ext/bada/__init__.py +6 -6
  18. pycontrails/ext/cirium/__init__.py +2 -2
  19. pycontrails/models/cocip/cocip.py +37 -39
  20. pycontrails/models/cocip/cocip_params.py +37 -30
  21. pycontrails/models/cocip/cocip_uncertainty.py +47 -58
  22. pycontrails/models/cocip/radiative_forcing.py +220 -193
  23. pycontrails/models/cocip/wake_vortex.py +96 -91
  24. pycontrails/models/cocip/wind_shear.py +2 -2
  25. pycontrails/models/emissions/emissions.py +1 -1
  26. pycontrails/models/humidity_scaling.py +266 -9
  27. pycontrails/models/issr.py +2 -2
  28. pycontrails/models/pcr.py +1 -1
  29. pycontrails/models/quantiles/era5_ensemble_quantiles.npy +0 -0
  30. pycontrails/models/quantiles/iagos_quantiles.npy +0 -0
  31. pycontrails/models/sac.py +7 -5
  32. pycontrails/physics/geo.py +5 -3
  33. pycontrails/physics/jet.py +66 -113
  34. pycontrails/utils/json.py +3 -3
  35. {pycontrails-0.41.0.dist-info → pycontrails-0.42.2.dist-info}/METADATA +4 -7
  36. {pycontrails-0.41.0.dist-info → pycontrails-0.42.2.dist-info}/RECORD +40 -34
  37. {pycontrails-0.41.0.dist-info → pycontrails-0.42.2.dist-info}/LICENSE +0 -0
  38. {pycontrails-0.41.0.dist-info → pycontrails-0.42.2.dist-info}/NOTICE +0 -0
  39. {pycontrails-0.41.0.dist-info → pycontrails-0.42.2.dist-info}/WHEEL +0 -0
  40. {pycontrails-0.41.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: