pycontrails 0.54.4__cp313-cp313-macosx_11_0_arm64.whl → 0.54.6__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.

Files changed (38) hide show
  1. pycontrails/_version.py +2 -2
  2. pycontrails/core/aircraft_performance.py +34 -16
  3. pycontrails/core/airports.py +3 -4
  4. pycontrails/core/fleet.py +30 -9
  5. pycontrails/core/flight.py +8 -5
  6. pycontrails/core/flightplan.py +11 -11
  7. pycontrails/core/interpolation.py +7 -4
  8. pycontrails/core/met.py +145 -86
  9. pycontrails/core/met_var.py +62 -0
  10. pycontrails/core/models.py +3 -2
  11. pycontrails/core/rgi_cython.cpython-313-darwin.so +0 -0
  12. pycontrails/core/vector.py +97 -74
  13. pycontrails/datalib/_met_utils/metsource.py +1 -1
  14. pycontrails/datalib/ecmwf/era5.py +5 -6
  15. pycontrails/datalib/ecmwf/era5_model_level.py +4 -5
  16. pycontrails/datalib/ecmwf/ifs.py +1 -3
  17. pycontrails/datalib/gfs/gfs.py +1 -3
  18. pycontrails/models/apcemm/apcemm.py +2 -2
  19. pycontrails/models/apcemm/utils.py +1 -1
  20. pycontrails/models/cocip/cocip.py +86 -27
  21. pycontrails/models/cocip/output_formats.py +1 -0
  22. pycontrails/models/cocipgrid/cocip_grid.py +8 -73
  23. pycontrails/models/dry_advection.py +99 -31
  24. pycontrails/models/emissions/emissions.py +2 -2
  25. pycontrails/models/humidity_scaling/humidity_scaling.py +1 -1
  26. pycontrails/models/issr.py +2 -2
  27. pycontrails/models/pcc.py +1 -2
  28. pycontrails/models/ps_model/ps_grid.py +2 -2
  29. pycontrails/models/ps_model/ps_model.py +4 -32
  30. pycontrails/models/ps_model/ps_operational_limits.py +2 -6
  31. pycontrails/models/tau_cirrus.py +13 -6
  32. pycontrails/physics/geo.py +3 -3
  33. {pycontrails-0.54.4.dist-info → pycontrails-0.54.6.dist-info}/METADATA +3 -4
  34. {pycontrails-0.54.4.dist-info → pycontrails-0.54.6.dist-info}/RECORD +38 -38
  35. {pycontrails-0.54.4.dist-info → pycontrails-0.54.6.dist-info}/WHEEL +1 -1
  36. {pycontrails-0.54.4.dist-info → pycontrails-0.54.6.dist-info}/LICENSE +0 -0
  37. {pycontrails-0.54.4.dist-info → pycontrails-0.54.6.dist-info}/NOTICE +0 -0
  38. {pycontrails-0.54.4.dist-info → pycontrails-0.54.6.dist-info}/top_level.txt +0 -0
pycontrails/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.54.4'
16
- __version_tuple__ = version_tuple = (0, 54, 4)
15
+ __version__ = version = '0.54.6'
16
+ __version_tuple__ = version_tuple = (0, 54, 6)
@@ -96,22 +96,52 @@ class AircraftPerformance(Model):
96
96
 
97
97
  source: Flight
98
98
 
99
- @abc.abstractmethod
100
99
  @overload
101
100
  def eval(self, source: Fleet, **params: Any) -> Fleet: ...
102
101
 
103
- @abc.abstractmethod
104
102
  @overload
105
103
  def eval(self, source: Flight, **params: Any) -> Flight: ...
106
104
 
107
- @abc.abstractmethod
108
105
  @overload
109
106
  def eval(self, source: None = ..., **params: Any) -> NoReturn: ...
110
107
 
111
- @abc.abstractmethod
112
108
  def eval(self, source: Flight | None = None, **params: Any) -> Flight:
113
109
  """Evaluate the aircraft performance model.
114
110
 
111
+ Parameters
112
+ ----------
113
+ source : Flight
114
+ Flight trajectory to evaluate. Can be a :class:`Flight` or :class:`Fleet`.
115
+ params : Any
116
+ Override :attr:`params` with keyword arguments.
117
+
118
+ Returns
119
+ -------
120
+ Flight
121
+ Flight trajectory with aircraft performance data.
122
+ """
123
+ self.update_params(params)
124
+ self.set_source(source)
125
+ self.source = self.require_source_type(Flight)
126
+ self.downselect_met()
127
+ self.set_source_met()
128
+ self._cleanup_indices()
129
+
130
+ # Calculate true airspeed if not included on source
131
+ self.ensure_true_airspeed_on_source()
132
+
133
+ if isinstance(self.source, Fleet):
134
+ fls = [self.eval_flight(fl) for fl in self.source.to_flight_list()]
135
+ self.source = Fleet.from_seq(fls, attrs=self.source.attrs, broadcast_numeric=False)
136
+ return self.source
137
+
138
+ self.source = self.eval_flight(self.source)
139
+ return self.source
140
+
141
+ @abc.abstractmethod
142
+ def eval_flight(self, fl: Flight) -> Flight:
143
+ """Evaluate the aircraft performance model on a single flight trajectory.
144
+
115
145
  The implementing model adds the following fields to the source flight:
116
146
 
117
147
  - ``aircraft_mass``: aircraft mass at each waypoint, [:math:`kg`]
@@ -128,18 +158,6 @@ class AircraftPerformance(Model):
128
158
  - ``max_mach``: maximum Mach number
129
159
  - ``max_altitude``: maximum altitude, [:math:`m`]
130
160
  - ``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
161
  """
144
162
 
145
163
  @override
@@ -162,7 +162,7 @@ def find_nearest_airport(
162
162
  ) & airports["latitude"].between((latitude - bbox), (latitude + bbox))
163
163
 
164
164
  # Find the nearest airport from largest to smallest airport type
165
- search_priority = ["large_airport", "medium_airport", "small_airport"]
165
+ search_priority = ("large_airport", "medium_airport", "small_airport")
166
166
 
167
167
  for airport_type in search_priority:
168
168
  is_airport_type = airports["type"] == airport_type
@@ -171,7 +171,7 @@ def find_nearest_airport(
171
171
  if len(nearest_airports) == 1:
172
172
  return nearest_airports["icao_code"].values[0]
173
173
 
174
- elif len(nearest_airports) > 1:
174
+ if len(nearest_airports) > 1:
175
175
  distance = distance_to_airports(
176
176
  nearest_airports,
177
177
  longitude,
@@ -181,8 +181,7 @@ def find_nearest_airport(
181
181
  i_nearest = np.argmin(distance)
182
182
  return nearest_airports["icao_code"].values[i_nearest]
183
183
 
184
- else:
185
- continue
184
+ continue
186
185
 
187
186
  return None
188
187
 
pycontrails/core/fleet.py CHANGED
@@ -133,18 +133,19 @@ class Fleet(Flight):
133
133
 
134
134
  @override
135
135
  def copy(self, **kwargs: Any) -> Self:
136
- kwargs.setdefault("fuel", self.fuel)
137
136
  kwargs.setdefault("fl_attrs", self.fl_attrs)
137
+ kwargs.setdefault("final_waypoints", self.final_waypoints)
138
138
  return super().copy(**kwargs)
139
139
 
140
140
  @override
141
141
  def filter(self, mask: npt.NDArray[np.bool_], copy: bool = True, **kwargs: Any) -> Self:
142
- kwargs.setdefault("fuel", self.fuel)
143
-
144
142
  flight_ids = set(np.unique(self["flight_id"][mask]))
145
143
  fl_attrs = {k: v for k, v in self.fl_attrs.items() if k in flight_ids}
146
144
  kwargs.setdefault("fl_attrs", fl_attrs)
147
145
 
146
+ final_waypoints = np.array(self.final_waypoints[mask], copy=copy)
147
+ kwargs.setdefault("final_waypoints", final_waypoints)
148
+
148
149
  return super().filter(mask, copy=copy, **kwargs)
149
150
 
150
151
  @override
@@ -187,8 +188,9 @@ class Fleet(Flight):
187
188
  in ``seq``.
188
189
  """
189
190
 
191
+ # Create a shallow copy because we add additional keys in _validate_fl
190
192
  def _shallow_copy(fl: Flight) -> Flight:
191
- return Flight(VectorDataDict(fl.data), attrs=fl.attrs, copy=False, fuel=fl.fuel)
193
+ return Flight._from_fastpath(fl.data, fl.attrs, fuel=fl.fuel)
192
194
 
193
195
  def _maybe_warn(fl: Flight) -> Flight:
194
196
  if not fl:
@@ -217,7 +219,18 @@ class Fleet(Flight):
217
219
  )
218
220
 
219
221
  data = {var: np.concatenate([fl[var] for fl in seq]) for var in seq[0]}
220
- return cls(data=data, attrs=attrs, copy=False, fuel=fuel, fl_attrs=fl_attrs)
222
+
223
+ final_waypoints = np.zeros(data["time"].size, dtype=bool)
224
+ final_waypoint_indices = np.cumsum([fl.size for fl in seq]) - 1
225
+ final_waypoints[final_waypoint_indices] = True
226
+
227
+ return cls._from_fastpath(
228
+ data,
229
+ attrs,
230
+ fuel=fuel,
231
+ fl_attrs=fl_attrs,
232
+ final_waypoints=final_waypoints,
233
+ )
221
234
 
222
235
  @property
223
236
  def n_flights(self) -> int:
@@ -246,11 +259,19 @@ class Fleet(Flight):
246
259
  List of Flights in the same order as was passed into the ``Fleet`` instance.
247
260
  """
248
261
  indices = self.dataframe.groupby("flight_id", sort=False).indices
262
+ if copy:
263
+ return [
264
+ Flight._from_fastpath(
265
+ {k: v[idx] for k, v in self.data.items()},
266
+ self.fl_attrs[flight_id],
267
+ fuel=self.fuel,
268
+ ).copy()
269
+ for flight_id, idx in indices.items()
270
+ ]
249
271
  return [
250
- Flight(
251
- data=VectorDataDict({k: v[idx] for k, v in self.data.items()}),
252
- attrs=self.fl_attrs[flight_id],
253
- copy=copy,
272
+ Flight._from_fastpath(
273
+ {k: v[idx] for k, v in self.data.items()},
274
+ self.fl_attrs[flight_id],
254
275
  fuel=self.fuel,
255
276
  )
256
277
  for flight_id, idx in indices.items()
@@ -891,7 +891,7 @@ class Flight(GeoVectorDataset):
891
891
  """
892
892
  methods = "geodesic", "linear"
893
893
  if fill_method not in methods:
894
- raise ValueError(f'Unknown `fill_method`. Supported methods: {", ".join(methods)}')
894
+ raise ValueError(f"Unknown `fill_method`. Supported methods: {', '.join(methods)}")
895
895
 
896
896
  # STEP 0: If self is empty, return an empty flight
897
897
  if not self:
@@ -957,7 +957,12 @@ class Flight(GeoVectorDataset):
957
957
  msg = f"{msg} Pass 'keep_original_index=True' to keep the original index."
958
958
  warnings.warn(msg)
959
959
 
960
- return type(self)(data=df, attrs=self.attrs, fuel=self.fuel)
960
+ # Reorder columns (this is unimportant but makes the output more canonical)
961
+ coord_names = ("longitude", "latitude", "altitude", "time")
962
+ df = df[[*coord_names, *[c for c in df.columns if c not in set(coord_names)]]]
963
+
964
+ data = {k: v.to_numpy() for k, v in df.items()}
965
+ return type(self)._from_fastpath(data, attrs=self.attrs, fuel=self.fuel)
961
966
 
962
967
  def clean_and_resample(
963
968
  self,
@@ -1977,9 +1982,7 @@ def filter_altitude(
1977
1982
  result[i0:i1] = altitude_filt[i0:i1]
1978
1983
 
1979
1984
  # reapply Savitzky-Golay filter to smooth climb and descent
1980
- result = _sg_filter(result, window_length=kernel_size)
1981
-
1982
- return result
1985
+ return _sg_filter(result, window_length=kernel_size)
1983
1986
 
1984
1987
 
1985
1988
  def segment_duration(
@@ -21,24 +21,24 @@ def to_atc_plan(plan: dict[str, Any]) -> str:
21
21
  --------
22
22
  :func:`parse_atc_plan`
23
23
  """
24
- ret = f'(FPL-{plan["callsign"]}-{plan["flight_rules"]}'
25
- ret += f'{plan["type_of_flight"]}\n'
24
+ ret = f"(FPL-{plan['callsign']}-{plan['flight_rules']}"
25
+ ret += f"{plan['type_of_flight']}\n"
26
26
  ret += "-"
27
27
  if "number_aircraft" in plan and plan["number_aircraft"] <= 10:
28
28
  ret += plan["number_aircraft"]
29
- ret += f'{plan["type_of_aircraft"]}/{plan["wake_category"]}-'
30
- ret += f'{plan["equipment"]}/{plan["transponder"]}\n'
31
- ret += f'-{plan["departure_icao"]}{plan["time"]}\n'
32
- ret += f'-{plan["speed_type"]}{plan["speed"]}{plan["level_type"]}'
33
- ret += f'{plan["level"]} {plan["route"]}\n'
29
+ ret += f"{plan['type_of_aircraft']}/{plan['wake_category']}-"
30
+ ret += f"{plan['equipment']}/{plan['transponder']}\n"
31
+ ret += f"-{plan['departure_icao']}{plan['time']}\n"
32
+ ret += f"-{plan['speed_type']}{plan['speed']}{plan['level_type']}"
33
+ ret += f"{plan['level']} {plan['route']}\n"
34
34
  if "destination_icao" in plan and "duration" in plan:
35
- ret += f'-{plan["destination_icao"]}{plan["duration"]}'
35
+ ret += f"-{plan['destination_icao']}{plan['duration']}"
36
36
  if "alt_icao" in plan:
37
- ret += f' {plan["alt_icao"]}'
37
+ ret += f" {plan['alt_icao']}"
38
38
  if "second_alt_icao" in plan:
39
- ret += f' {plan["second_alt_icao"]}'
39
+ ret += f" {plan['second_alt_icao']}"
40
40
  ret += "\n"
41
- ret += f'-{plan["other_info"]})\n'
41
+ ret += f"-{plan['other_info']})\n"
42
42
  if "supplementary_info" in plan:
43
43
  ret += " ".join([f"{i[0]}/{i[1]}" for i in plan["supplementary_info"].items()])
44
44
 
@@ -245,6 +245,9 @@ def _pick_method(scipy_version: str, method: str) -> str:
245
245
  str
246
246
  Interpolation method adjusted for compatibility with this class.
247
247
  """
248
+ if method == "linear":
249
+ return method
250
+
248
251
  try:
249
252
  version = scipy_version.split(".")
250
253
  major = int(version[0])
@@ -486,15 +489,15 @@ def interp(
486
489
  da = _localize(da, coords)
487
490
 
488
491
  indexes = da._indexes
489
- x = indexes["longitude"].index.to_numpy() # type: ignore[attr-defined]
490
- y = indexes["latitude"].index.to_numpy() # type: ignore[attr-defined]
491
- z = indexes["level"].index.to_numpy() # type: ignore[attr-defined]
492
+ x = indexes["longitude"].index.values # type: ignore[attr-defined]
493
+ y = indexes["latitude"].index.values # type: ignore[attr-defined]
494
+ z = indexes["level"].index.values # type: ignore[attr-defined]
492
495
  if any(v.dtype != np.float64 for v in (x, y, z)):
493
496
  msg = "da must have float64 dtype for longitude, latitude, and level coordinates"
494
497
  raise ValueError(msg)
495
498
 
496
499
  # Convert t and time to float64
497
- t = indexes["time"].index.to_numpy() # type: ignore[attr-defined]
500
+ t = indexes["time"].index.values # type: ignore[attr-defined]
498
501
  offset = t[0]
499
502
  t = _floatize_time(t, offset)
500
503