pycontrails 0.54.3__cp310-cp310-win_amd64.whl → 0.54.5__cp310-cp310-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.

Files changed (62) hide show
  1. pycontrails/__init__.py +2 -2
  2. pycontrails/_version.py +2 -2
  3. pycontrails/core/__init__.py +1 -1
  4. pycontrails/core/aircraft_performance.py +58 -58
  5. pycontrails/core/cache.py +7 -7
  6. pycontrails/core/fleet.py +54 -29
  7. pycontrails/core/flight.py +218 -301
  8. pycontrails/core/interpolation.py +63 -60
  9. pycontrails/core/met.py +193 -125
  10. pycontrails/core/models.py +27 -13
  11. pycontrails/core/polygon.py +15 -15
  12. pycontrails/core/rgi_cython.cp310-win_amd64.pyd +0 -0
  13. pycontrails/core/vector.py +119 -96
  14. pycontrails/datalib/_met_utils/metsource.py +8 -5
  15. pycontrails/datalib/ecmwf/__init__.py +14 -14
  16. pycontrails/datalib/ecmwf/common.py +1 -1
  17. pycontrails/datalib/ecmwf/era5.py +7 -7
  18. pycontrails/datalib/ecmwf/hres.py +3 -3
  19. pycontrails/datalib/ecmwf/ifs.py +1 -1
  20. pycontrails/datalib/gfs/__init__.py +6 -6
  21. pycontrails/datalib/gfs/gfs.py +2 -2
  22. pycontrails/datalib/goes.py +5 -5
  23. pycontrails/ext/empirical_grid.py +1 -1
  24. pycontrails/models/apcemm/apcemm.py +5 -5
  25. pycontrails/models/apcemm/utils.py +1 -1
  26. pycontrails/models/cocip/__init__.py +2 -2
  27. pycontrails/models/cocip/cocip.py +23 -24
  28. pycontrails/models/cocip/cocip_params.py +2 -11
  29. pycontrails/models/cocip/cocip_uncertainty.py +24 -18
  30. pycontrails/models/cocip/contrail_properties.py +331 -316
  31. pycontrails/models/cocip/output_formats.py +53 -53
  32. pycontrails/models/cocip/radiative_forcing.py +135 -131
  33. pycontrails/models/cocip/radiative_heating.py +135 -135
  34. pycontrails/models/cocip/unterstrasser_wake_vortex.py +90 -87
  35. pycontrails/models/cocip/wake_vortex.py +92 -92
  36. pycontrails/models/cocip/wind_shear.py +8 -8
  37. pycontrails/models/cocipgrid/cocip_grid.py +37 -96
  38. pycontrails/models/dry_advection.py +60 -19
  39. pycontrails/models/emissions/__init__.py +2 -2
  40. pycontrails/models/emissions/black_carbon.py +108 -108
  41. pycontrails/models/emissions/emissions.py +87 -87
  42. pycontrails/models/emissions/ffm2.py +35 -35
  43. pycontrails/models/humidity_scaling/humidity_scaling.py +23 -23
  44. pycontrails/models/issr.py +2 -2
  45. pycontrails/models/ps_model/__init__.py +1 -1
  46. pycontrails/models/ps_model/ps_aircraft_params.py +8 -4
  47. pycontrails/models/ps_model/ps_grid.py +76 -66
  48. pycontrails/models/ps_model/ps_model.py +16 -16
  49. pycontrails/models/ps_model/ps_operational_limits.py +20 -18
  50. pycontrails/models/tau_cirrus.py +8 -1
  51. pycontrails/physics/geo.py +67 -67
  52. pycontrails/physics/jet.py +79 -79
  53. pycontrails/physics/units.py +14 -14
  54. pycontrails/utils/json.py +1 -2
  55. pycontrails/utils/types.py +12 -7
  56. {pycontrails-0.54.3.dist-info → pycontrails-0.54.5.dist-info}/METADATA +2 -2
  57. {pycontrails-0.54.3.dist-info → pycontrails-0.54.5.dist-info}/NOTICE +1 -1
  58. pycontrails-0.54.5.dist-info/RECORD +111 -0
  59. pycontrails-0.54.3.dist-info/RECORD +0 -111
  60. {pycontrails-0.54.3.dist-info → pycontrails-0.54.5.dist-info}/LICENSE +0 -0
  61. {pycontrails-0.54.3.dist-info → pycontrails-0.54.5.dist-info}/WHEEL +0 -0
  62. {pycontrails-0.54.3.dist-info → pycontrails-0.54.5.dist-info}/top_level.txt +0 -0
@@ -6,13 +6,18 @@ import enum
6
6
  import logging
7
7
  import sys
8
8
  import warnings
9
- from typing import TYPE_CHECKING, Any, NoReturn, TypeVar
9
+ from typing import TYPE_CHECKING, Any, NoReturn
10
10
 
11
11
  if sys.version_info >= (3, 12):
12
12
  from typing import override
13
13
  else:
14
14
  from typing_extensions import override
15
15
 
16
+ if sys.version_info >= (3, 11):
17
+ from typing import Self
18
+ else:
19
+ from typing_extensions import Self
20
+
16
21
 
17
22
  import numpy as np
18
23
  import numpy.typing as npt
@@ -27,8 +32,6 @@ from pycontrails.utils.types import ArrayOrFloat
27
32
 
28
33
  logger = logging.getLogger(__name__)
29
34
 
30
- FlightType = TypeVar("FlightType", bound="Flight")
31
-
32
35
  # optional imports
33
36
  if TYPE_CHECKING:
34
37
  import matplotlib.axes
@@ -285,14 +288,12 @@ class Flight(GeoVectorDataset):
285
288
  )
286
289
 
287
290
  @override
288
- def copy(self: FlightType, **kwargs: Any) -> FlightType:
291
+ def copy(self, **kwargs: Any) -> Self:
289
292
  kwargs.setdefault("fuel", self.fuel)
290
293
  return super().copy(**kwargs)
291
294
 
292
295
  @override
293
- def filter(
294
- self: FlightType, mask: npt.NDArray[np.bool_], copy: bool = True, **kwargs: Any
295
- ) -> FlightType:
296
+ def filter(self, mask: npt.NDArray[np.bool_], copy: bool = True, **kwargs: Any) -> Self:
296
297
  kwargs.setdefault("fuel", self.fuel)
297
298
  return super().filter(mask, copy=copy, **kwargs)
298
299
 
@@ -414,7 +415,7 @@ class Flight(GeoVectorDataset):
414
415
  # Segment Properties
415
416
  # ------------
416
417
 
417
- def segment_duration(self, dtype: npt.DTypeLike = np.float32) -> npt.NDArray[np.float64]:
418
+ def segment_duration(self, dtype: npt.DTypeLike = np.float32) -> npt.NDArray[np.floating]:
418
419
  r"""Compute time elapsed between waypoints in seconds.
419
420
 
420
421
  ``np.nan`` appended so the length of the output is the same as number of waypoints.
@@ -427,14 +428,14 @@ class Flight(GeoVectorDataset):
427
428
 
428
429
  Returns
429
430
  -------
430
- npt.NDArray[np.float64]
431
+ npt.NDArray[np.floating]
431
432
  Time difference between waypoints, [:math:`s`].
432
433
  Returns an array with dtype specified by``dtype``
433
434
  """
434
435
 
435
436
  return segment_duration(self.data["time"], dtype=dtype)
436
437
 
437
- def segment_haversine(self) -> npt.NDArray[np.float64]:
438
+ def segment_haversine(self) -> npt.NDArray[np.floating]:
438
439
  """Compute Haversine (great circle) distance between flight waypoints.
439
440
 
440
441
  Helper function used in :meth:`resample_and_fill`.
@@ -445,7 +446,7 @@ class Flight(GeoVectorDataset):
445
446
 
446
447
  Returns
447
448
  -------
448
- npt.NDArray[np.float64]
449
+ npt.NDArray[np.floating]
449
450
  Array of great circle distances in [:math:`m`] between waypoints
450
451
 
451
452
  Examples
@@ -468,7 +469,7 @@ class Flight(GeoVectorDataset):
468
469
  """
469
470
  return geo.segment_haversine(self["longitude"], self["latitude"])
470
471
 
471
- def segment_length(self) -> npt.NDArray[np.float64]:
472
+ def segment_length(self) -> npt.NDArray[np.floating]:
472
473
  """Compute spherical distance between flight waypoints.
473
474
 
474
475
  Helper function used in :meth:`length` and :meth:`length_met`.
@@ -476,7 +477,7 @@ class Flight(GeoVectorDataset):
476
477
 
477
478
  Returns
478
479
  -------
479
- npt.NDArray[np.float64]
480
+ npt.NDArray[np.floating]
480
481
  Array of distances in [:math:`m`] between waypoints
481
482
 
482
483
  Examples
@@ -498,7 +499,7 @@ class Flight(GeoVectorDataset):
498
499
  """
499
500
  return geo.segment_length(self["longitude"], self["latitude"], self.altitude)
500
501
 
501
- def segment_angle(self) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
502
+ def segment_angle(self) -> tuple[npt.NDArray[np.floating], npt.NDArray[np.floating]]:
502
503
  """Calculate sine and cosine for the angle between each segment and the longitudinal axis.
503
504
 
504
505
  This is different from the usual navigational angle between two points known as *bearing*.
@@ -518,7 +519,7 @@ class Flight(GeoVectorDataset):
518
519
 
519
520
  Returns
520
521
  -------
521
- npt.NDArray[np.float64], npt.NDArray[np.float64]
522
+ npt.NDArray[np.floating], npt.NDArray[np.floating]
522
523
  Returns ``sin(a), cos(a)``, where ``a`` is the angle between the segment and the
523
524
  longitudinal axis. The final values are of both arrays are ``np.nan``.
524
525
 
@@ -548,7 +549,7 @@ class Flight(GeoVectorDataset):
548
549
  """
549
550
  return geo.segment_angle(self["longitude"], self["latitude"])
550
551
 
551
- def segment_azimuth(self) -> npt.NDArray[np.float64]:
552
+ def segment_azimuth(self) -> npt.NDArray[np.floating]:
552
553
  """Calculate (forward) azimuth at each waypoint.
553
554
 
554
555
  Method calls `pyproj.Geod.inv`, which is slow. See `geo.forward_azimuth`
@@ -560,7 +561,7 @@ class Flight(GeoVectorDataset):
560
561
 
561
562
  Returns
562
563
  -------
563
- npt.NDArray[np.float64]
564
+ npt.NDArray[np.floating]
564
565
  Array of azimuths.
565
566
 
566
567
  See Also
@@ -599,7 +600,7 @@ class Flight(GeoVectorDataset):
599
600
 
600
601
  def segment_groundspeed(
601
602
  self, smooth: bool = False, window_length: int = 7, polyorder: int = 1
602
- ) -> npt.NDArray[np.float64]:
603
+ ) -> npt.NDArray[np.floating]:
603
604
  """Return groundspeed across segments.
604
605
 
605
606
  Calculate by dividing the horizontal segment length by the difference in waypoint times.
@@ -616,7 +617,7 @@ class Flight(GeoVectorDataset):
616
617
 
617
618
  Returns
618
619
  -------
619
- npt.NDArray[np.float64]
620
+ npt.NDArray[np.floating]
620
621
  Groundspeed of the segment, [:math:`m s^{-1}`]
621
622
  """
622
623
  # get horizontal distance (altitude is ignored)
@@ -638,22 +639,22 @@ class Flight(GeoVectorDataset):
638
639
 
639
640
  def segment_true_airspeed(
640
641
  self,
641
- u_wind: npt.NDArray[np.float64] | float = 0.0,
642
- v_wind: npt.NDArray[np.float64] | float = 0.0,
642
+ u_wind: npt.NDArray[np.floating] | float = 0.0,
643
+ v_wind: npt.NDArray[np.floating] | float = 0.0,
643
644
  smooth: bool = True,
644
645
  window_length: int = 7,
645
646
  polyorder: int = 1,
646
- ) -> npt.NDArray[np.float64]:
647
+ ) -> npt.NDArray[np.floating]:
647
648
  r"""Calculate the true airspeed [:math:`m/s`] from the ground speed and horizontal winds.
648
649
 
649
650
  The calculated ground speed will first be smoothed with a Savitzky-Golay filter if enabled.
650
651
 
651
652
  Parameters
652
653
  ----------
653
- u_wind : npt.NDArray[np.float64] | float
654
+ u_wind : npt.NDArray[np.floating] | float
654
655
  U wind speed, [:math:`m \ s^{-1}`].
655
656
  Defaults to 0 for all waypoints.
656
- v_wind : npt.NDArray[np.float64] | float
657
+ v_wind : npt.NDArray[np.floating] | float
657
658
  V wind speed, [:math:`m \ s^{-1}`].
658
659
  Defaults to 0 for all waypoints.
659
660
  smooth : bool, optional
@@ -666,7 +667,7 @@ class Flight(GeoVectorDataset):
666
667
 
667
668
  Returns
668
669
  -------
669
- npt.NDArray[np.float64]
670
+ npt.NDArray[np.floating]
670
671
  True wind speed of each segment, [:math:`m \ s^{-1}`]
671
672
  """
672
673
  groundspeed = self.segment_groundspeed(smooth, window_length, polyorder)
@@ -680,39 +681,39 @@ class Flight(GeoVectorDataset):
680
681
  return np.sqrt(tas_x * tas_x + tas_y * tas_y)
681
682
 
682
683
  def segment_mach_number(
683
- self, true_airspeed: npt.NDArray[np.float64], air_temperature: npt.NDArray[np.float64]
684
- ) -> npt.NDArray[np.float64]:
684
+ self, true_airspeed: npt.NDArray[np.floating], air_temperature: npt.NDArray[np.floating]
685
+ ) -> npt.NDArray[np.floating]:
685
686
  r"""Calculate the mach number of each segment.
686
687
 
687
688
  Parameters
688
689
  ----------
689
- true_airspeed : npt.NDArray[np.float64]
690
+ true_airspeed : npt.NDArray[np.floating]
690
691
  True airspeed of the segment, [:math:`m \ s^{-1}`].
691
692
  See :meth:`segment_true_airspeed`.
692
- air_temperature : npt.NDArray[np.float64]
693
+ air_temperature : npt.NDArray[np.floating]
693
694
  Average air temperature of each segment, [:math:`K`]
694
695
 
695
696
  Returns
696
697
  -------
697
- npt.NDArray[np.float64]
698
+ npt.NDArray[np.floating]
698
699
  Mach number of each segment
699
700
  """
700
701
  return units.tas_to_mach_number(true_airspeed, air_temperature)
701
702
 
702
703
  def segment_rocd(
703
704
  self,
704
- air_temperature: None | npt.NDArray[np.float64] = None,
705
- ) -> npt.NDArray[np.float64]:
705
+ air_temperature: None | npt.NDArray[np.floating] = None,
706
+ ) -> npt.NDArray[np.floating]:
706
707
  """Calculate the rate of climb and descent (ROCD).
707
708
 
708
709
  Parameters
709
710
  ----------
710
- air_temperature: None | npt.NDArray[np.float64]
711
+ air_temperature: None | npt.NDArray[np.floating]
711
712
  Air temperature of each flight waypoint, [:math:`K`]
712
713
 
713
714
  Returns
714
715
  -------
715
- npt.NDArray[np.float64]
716
+ npt.NDArray[np.floating]
716
717
  Rate of climb and descent over segment, [:math:`ft min^{-1}`]
717
718
 
718
719
  See Also
@@ -725,7 +726,7 @@ class Flight(GeoVectorDataset):
725
726
  self,
726
727
  threshold_rocd: float = 250.0,
727
728
  min_cruise_altitude_ft: float = 20000.0,
728
- air_temperature: None | npt.NDArray[np.float64] = None,
729
+ air_temperature: None | npt.NDArray[np.floating] = None,
729
730
  ) -> npt.NDArray[np.uint8]:
730
731
  """Identify the phase of flight (climb, cruise, descent) for each segment.
731
732
 
@@ -739,7 +740,7 @@ class Flight(GeoVectorDataset):
739
740
  This is specific for each aircraft type,
740
741
  and can be approximated as 50% of the altitude ceiling.
741
742
  Defaults to 20000 ft.
742
- air_temperature: None | npt.NDArray[np.float64]
743
+ air_temperature: None | npt.NDArray[np.floating]
743
744
  Air temperature of each flight waypoint, [:math:`K`]
744
745
 
745
746
  Returns
@@ -765,7 +766,7 @@ class Flight(GeoVectorDataset):
765
766
  # Filter/Resample
766
767
  # ------------
767
768
 
768
- def filter_by_first(self) -> Flight:
769
+ def filter_by_first(self) -> Self:
769
770
  """Keep first row of group of waypoints with identical coordinates.
770
771
 
771
772
  Chaining this method with `resample_and_fill` often gives a cleaner trajectory
@@ -795,7 +796,7 @@ class Flight(GeoVectorDataset):
795
796
  1 50.0 0.0 0.0 2020-01-01 02:00:00
796
797
  """
797
798
  df = self.dataframe.groupby(["longitude", "latitude"], sort=False).first().reset_index()
798
- return Flight(data=df, attrs=self.attrs)
799
+ return type(self)(data=df, attrs=self.attrs, fuel=self.fuel)
799
800
 
800
801
  def resample_and_fill(
801
802
  self,
@@ -805,8 +806,7 @@ class Flight(GeoVectorDataset):
805
806
  nominal_rocd: float = constants.nominal_rocd,
806
807
  drop: bool = True,
807
808
  keep_original_index: bool = False,
808
- climb_descend_at_end: bool = False,
809
- ) -> Flight:
809
+ ) -> Self:
810
810
  """Resample and fill flight trajectory with geodesics and linear interpolation.
811
811
 
812
812
  Waypoints are resampled according to the frequency ``freq``. Values for :attr:`data`
@@ -844,9 +844,6 @@ class Flight(GeoVectorDataset):
844
844
  Keep the original index of the :class:`Flight` in addition to the new
845
845
  resampled index. Defaults to ``False``.
846
846
  .. versionadded:: 0.45.2
847
- climb_or_descend_at_end : bool
848
- If true, the climb or descent will be placed at the end of each segment
849
- rather than the start. Default is false (climb or descent immediately).
850
847
 
851
848
  Returns
852
849
  -------
@@ -940,16 +937,13 @@ class Flight(GeoVectorDataset):
940
937
 
941
938
  if shift is not None:
942
939
  # We need to translate back to the original chart here
943
- df["longitude"] += shift
944
- df["longitude"] = ((df["longitude"] + 180.0) % 360.0) - 180.0
940
+ df["longitude"] = ((df["longitude"] + shift + 180.0) % 360.0) - 180.0
945
941
 
946
942
  # STEP 6: Interpolate nan values in altitude
947
943
  altitude = df["altitude"].to_numpy()
944
+ time = df.index.to_numpy()
948
945
  if np.any(np.isnan(altitude)):
949
- df_freq = pd.Timedelta(freq).to_numpy()
950
- new_alt = _altitude_interpolation(altitude, nominal_rocd, df_freq, climb_descend_at_end)
951
- _verify_altitude(new_alt, nominal_rocd, df_freq)
952
- df["altitude"] = new_alt
946
+ df["altitude"] = _altitude_interpolation(altitude, time, nominal_rocd)
953
947
 
954
948
  # Remove original index if requested
955
949
  if not keep_original_index:
@@ -963,7 +957,12 @@ class Flight(GeoVectorDataset):
963
957
  msg = f"{msg} Pass 'keep_original_index=True' to keep the original index."
964
958
  warnings.warn(msg)
965
959
 
966
- return Flight(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)
967
966
 
968
967
  def clean_and_resample(
969
968
  self,
@@ -976,8 +975,7 @@ class Flight(GeoVectorDataset):
976
975
  force_filter: bool = False,
977
976
  drop: bool = True,
978
977
  keep_original_index: bool = False,
979
- climb_descend_at_end: bool = False,
980
- ) -> Flight:
978
+ ) -> Self:
981
979
  """Resample and (possibly) filter a flight trajectory.
982
980
 
983
981
  Waypoints are resampled according to the frequency ``freq``. If the original
@@ -1018,9 +1016,6 @@ class Flight(GeoVectorDataset):
1018
1016
  Keep the original index of the :class:`Flight` in addition to the new
1019
1017
  resampled index. Defaults to ``False``.
1020
1018
  .. versionadded:: 0.45.2
1021
- climb_or_descend_at_end : bool
1022
- If true, the climb or descent will be placed at the end of each segment
1023
- rather than the start. Default is false (climb or descent immediately).
1024
1019
 
1025
1020
  Returns
1026
1021
  -------
@@ -1039,7 +1034,6 @@ class Flight(GeoVectorDataset):
1039
1034
  nominal_rocd,
1040
1035
  drop,
1041
1036
  keep_original_index,
1042
- climb_descend_at_end,
1043
1037
  )
1044
1038
 
1045
1039
  # If the flight has large gap(s), then call resample and fill, then filter altitude
@@ -1055,7 +1049,6 @@ class Flight(GeoVectorDataset):
1055
1049
  nominal_rocd,
1056
1050
  drop,
1057
1051
  keep_original_index,
1058
- climb_descend_at_end,
1059
1052
  )
1060
1053
  clean_flight = clean_flight.filter_altitude(kernel_size, cruise_threshold)
1061
1054
  else:
@@ -1069,14 +1062,13 @@ class Flight(GeoVectorDataset):
1069
1062
  nominal_rocd,
1070
1063
  drop,
1071
1064
  keep_original_index,
1072
- climb_descend_at_end,
1073
1065
  )
1074
1066
 
1075
1067
  def filter_altitude(
1076
1068
  self,
1077
1069
  kernel_size: int = 17,
1078
1070
  cruise_threshold: float = 120.0,
1079
- ) -> Flight:
1071
+ ) -> Self:
1080
1072
  """
1081
1073
  Filter noisy altitude on a single flight.
1082
1074
 
@@ -1508,7 +1500,7 @@ class Flight(GeoVectorDataset):
1508
1500
  >>> fl["air_temperature"] = fl.intersect_met(met["air_temperature"])
1509
1501
  >>> fl["air_temperature"]
1510
1502
  array([235.94657007, 235.55745645, 235.56709768, ..., 234.59917962,
1511
- 234.60387402, 234.60845312])
1503
+ 234.60387402, 234.60845312], shape=(1081,))
1512
1504
 
1513
1505
  >>> # Length (in meters) of waypoints whose temperature exceeds 236K
1514
1506
  >>> fl.length_met("air_temperature", threshold=236)
@@ -1606,12 +1598,12 @@ class Flight(GeoVectorDataset):
1606
1598
  return ax
1607
1599
 
1608
1600
 
1609
- def _return_linestring(data: dict[str, npt.NDArray[np.float64]]) -> list[list[float]]:
1601
+ def _return_linestring(data: dict[str, npt.NDArray[np.floating]]) -> list[list[float]]:
1610
1602
  """Return list of coordinates for geojson constructions.
1611
1603
 
1612
1604
  Parameters
1613
1605
  ----------
1614
- data : dict[str, npt.NDArray[np.float64]]
1606
+ data : dict[str, npt.NDArray[np.floating]]
1615
1607
  :attr:`data` containing `longitude`, `latitude`, and `altitude` keys
1616
1608
 
1617
1609
  Returns
@@ -1659,15 +1651,15 @@ def _antimeridian_index(longitude: pd.Series) -> list[int]:
1659
1651
 
1660
1652
 
1661
1653
  def _sg_filter(
1662
- vals: npt.NDArray[np.float64], window_length: int = 7, polyorder: int = 1
1663
- ) -> npt.NDArray[np.float64]:
1654
+ vals: npt.NDArray[np.floating], window_length: int = 7, polyorder: int = 1
1655
+ ) -> npt.NDArray[np.floating]:
1664
1656
  """Apply Savitzky-Golay filter to smooth out noise in the time-series data.
1665
1657
 
1666
1658
  Used to smooth true airspeed, fuel flow, and altitude.
1667
1659
 
1668
1660
  Parameters
1669
1661
  ----------
1670
- vals : npt.NDArray[np.float64]
1662
+ vals : npt.NDArray[np.floating]
1671
1663
  Input array
1672
1664
  window_length : int, optional
1673
1665
  Parameter for :func:`scipy.signal.savgol_filter`
@@ -1676,7 +1668,7 @@ def _sg_filter(
1676
1668
 
1677
1669
  Returns
1678
1670
  -------
1679
- npt.NDArray[np.float64]
1671
+ npt.NDArray[np.floating]
1680
1672
  Smoothed values
1681
1673
 
1682
1674
  Raises
@@ -1702,11 +1694,12 @@ def _sg_filter(
1702
1694
 
1703
1695
 
1704
1696
  def _altitude_interpolation(
1705
- altitude: npt.NDArray[np.float64],
1697
+ altitude: npt.NDArray[np.floating],
1698
+ time: npt.NDArray[np.datetime64],
1706
1699
  nominal_rocd: float,
1707
- freq: np.timedelta64,
1708
- climb_or_descend_at_end: bool = False,
1709
- ) -> npt.NDArray[np.float64]:
1700
+ minimum_cruise_altitude_ft: float = 20000.0,
1701
+ assumed_cruise_altitude_ft: float = 30000.0,
1702
+ ) -> npt.NDArray[np.floating]:
1710
1703
  """Interpolate nan values in ``altitude`` array.
1711
1704
 
1712
1705
  Suppose each group of consecutive nan values is enclosed by ``a0`` and ``a1`` with
@@ -1718,31 +1711,50 @@ def _altitude_interpolation(
1718
1711
 
1719
1712
  Parameters
1720
1713
  ----------
1721
- altitude : npt.NDArray[np.float64]
1714
+ altitude : npt.NDArray[np.floating]
1722
1715
  Array of altitude values containing nan values. This function will raise
1723
1716
  an error if ``altitude`` does not contain nan values. Moreover, this function
1724
- assumes the initial and final entries in ``altitude`` are not nan.
1717
+ assumes the initial and final entries in ``altitude`` are not nan, [:math:`m`]
1718
+ time : npt.NDArray[np.datetime64]
1719
+ Timestamp at each waypoint. Must be monotonically increasing.
1725
1720
  nominal_rocd : float
1726
- Nominal rate of climb/descent, in m/s
1727
- freq : np.timedelta64
1728
- Frequency of time index associated to ``altitude``.
1729
- climb_or_descend_at_end : bool
1730
- If true, the climb or descent will be placed at the end of each segment
1731
- rather than the start. Default is false (climb or descent immediately).
1721
+ Nominal rate of climb/descent, [:math:`m s^{-1}`]
1722
+ minimum_cruise_altitude_ft : float
1723
+ Minimium cruising altitude for a given aircraft type, [:math:`ft`].
1724
+ By default, this is 20000.0 ft.
1725
+ assumed_cruise_altitude_ft : float
1726
+ Assumed cruising altitude for a given aircraft type, [:math:`ft`].
1727
+ By default, this is 30000.0 ft.
1732
1728
 
1733
1729
  Returns
1734
1730
  -------
1735
- npt.NDArray[np.float64]
1736
- Altitude after nan values have been filled
1731
+ npt.NDArray[np.floating]
1732
+ Altitude after nan values have been filled, [:math:`m`]
1733
+
1734
+ Notes
1735
+ -----
1736
+ Default values for ``minimum_cruise_altitude_ft`` and ``assumed_cruise_altitude_ft`` should be
1737
+ provided if aircraft-specific parameters are available to improve the output quality.
1738
+
1739
+ We can assume ``minimum_cruise_altitude_ft`` as 0.5 times the aircraft service ceiling, and
1740
+ ``assumed_cruise_altitude_ft`` as 0.8 times the aircraft service ceiling.
1741
+
1742
+ Assume that aircraft will generally prefer to climb to a higher altitude as early as possible,
1743
+ and descent to a lower altitude as late as possible, because a higher altitude can reduce
1744
+ drag and fuel consumption.
1737
1745
  """
1746
+ # Work in units of feet
1747
+ alt_ft = units.m_to_ft(altitude)
1748
+ nominal_rocd_ft_min = units.m_to_ft(nominal_rocd) * 60.0
1749
+
1738
1750
  # Determine nan state of altitude
1739
- isna = np.isnan(altitude)
1751
+ isna = np.isnan(alt_ft)
1740
1752
 
1741
- start_na = np.empty(altitude.size, dtype=bool)
1753
+ start_na = np.empty(alt_ft.size, dtype=bool)
1742
1754
  start_na[:-1] = ~isna[:-1] & isna[1:]
1743
1755
  start_na[-1] = False
1744
1756
 
1745
- end_na = np.empty(altitude.size, dtype=bool)
1757
+ end_na = np.empty(alt_ft.size, dtype=bool)
1746
1758
  end_na[0] = False
1747
1759
  end_na[1:] = isna[:-1] & ~isna[1:]
1748
1760
 
@@ -1751,214 +1763,119 @@ def _altitude_interpolation(
1751
1763
  end_na_idxs = np.flatnonzero(end_na)
1752
1764
  na_group_size = end_na_idxs - start_na_idxs
1753
1765
 
1754
- if climb_or_descend_at_end:
1755
- return _altitude_interpolation_climb_descend_end(
1756
- altitude, na_group_size, nominal_rocd, freq, isna
1766
+ # NOTE: Only fill altitude gaps that require special attention
1767
+ # At the end of this for loop, those with NaN altitudes will be filled with pd.interpolate
1768
+ for i in range(len(na_group_size)):
1769
+ alt_ft_start = alt_ft[start_na_idxs[i]]
1770
+ alt_ft_end = alt_ft[end_na_idxs[i]]
1771
+ time_start = time[start_na_idxs[i]]
1772
+ time_end = time[end_na_idxs[i]]
1773
+
1774
+ # Calculate parameters to determine how to interpolate altitude
1775
+ # Time to next waypoint
1776
+ dt_next = (time_end - time_start) / np.timedelta64(1, "m")
1777
+
1778
+ # (1): Unrealistic scenario: first and next known waypoints are at very
1779
+ # low altitudes with a large time gap.
1780
+ is_unrealistic = (
1781
+ dt_next > 60.0
1782
+ and alt_ft_start < minimum_cruise_altitude_ft
1783
+ and alt_ft_end < minimum_cruise_altitude_ft
1757
1784
  )
1758
1785
 
1759
- return _altitude_interpolation_climb_descend_middle(
1760
- altitude, start_na_idxs, end_na_idxs, na_group_size, freq, nominal_rocd, isna
1761
- )
1762
-
1763
-
1764
- def _altitude_interpolation_climb_descend_end(
1765
- altitude: npt.NDArray[np.float64],
1766
- na_group_size: npt.NDArray[np.intp],
1767
- nominal_rocd: float,
1768
- freq: np.timedelta64,
1769
- isna: npt.NDArray[np.bool_],
1770
- ) -> npt.NDArray[np.float64]:
1771
- """Interpolate altitude values by placing climbs/descents at end of nan sequences.
1772
-
1773
- The segment will remain at constant elevation until the end of the segment where
1774
- it will climb or descend at a constant rocd based on `nominal_rocd`, reaching the
1775
- target altitude at the end of the segment.
1776
-
1777
- Parameters
1778
- ----------
1779
- altitude : npt.NDArray[np.float64]
1780
- Array of altitude values containing nan values. This function will raise
1781
- an error if ``altitude`` does not contain nan values. Moreover, this function
1782
- assumes the initial and final entries in ``altitude`` are not nan.
1783
- na_group_size : npt.NDArray[np.intp]
1784
- Array of the length of each consecutive sequence of nan values within the
1785
- array provided to input parameter `altitude`.
1786
- nominal_rocd : float
1787
- Nominal rate of climb/descent, in m/s
1788
- freq : np.timedelta64
1789
- Frequency of time index associated to ``altitude``.
1790
- isna : npt.NDArray[np.bool_]
1791
- Array of boolean values indicating whether or not each entry in `altitude`
1792
- is nan-valued.
1793
- -------
1794
- npt.NDArray[np.float64]
1795
- Altitude after nan values have been filled
1796
- """
1797
- cumalt_list = [np.flip(np.arange(1, size, dtype=float)) for size in na_group_size]
1798
- cumalt = np.concatenate(cumalt_list)
1799
- cumalt = cumalt * nominal_rocd * (freq / np.timedelta64(1, "s"))
1800
-
1801
- # Expand cumalt to the full size of altitude
1802
- nominal_fill = np.zeros_like(altitude)
1803
- nominal_fill[isna] = cumalt
1804
-
1805
- # Use pandas to forward and backfill altitude values
1806
- s = pd.Series(altitude)
1807
- s_ff = s.ffill()
1808
- s_bf = s.bfill()
1809
-
1810
- # Construct altitude values if the flight were to climb / descent throughout
1811
- # group of consecutive nan values. The call to np.minimum / np.maximum cuts
1812
- # the climb / descent off at the terminal altitude of the nan group
1813
- fill_climb = np.maximum(s_ff, s_bf - nominal_fill)
1814
- fill_descent = np.minimum(s_ff, s_bf + nominal_fill)
1815
-
1816
- # Explicitly determine if the flight is in a climb or descent state
1817
- sign = np.full_like(altitude, np.nan)
1818
- sign[~isna] = np.sign(np.diff(altitude[~isna], append=np.nan))
1819
- sign = pd.Series(sign).ffill()
1820
-
1821
- # And return the mess
1822
- return np.where(sign == 1.0, fill_climb, fill_descent)
1823
-
1824
-
1825
- def _altitude_interpolation_climb_descend_middle(
1826
- altitude: npt.NDArray[np.float64],
1827
- start_na_idxs: npt.NDArray[np.intp],
1828
- end_na_idxs: npt.NDArray[np.intp],
1829
- na_group_size: npt.NDArray[np.intp],
1830
- freq: np.timedelta64,
1831
- nominal_rocd: float,
1832
- isna: npt.NDArray[np.bool_],
1833
- ) -> npt.NDArray[np.float64]:
1834
- """Interpolate nan altitude values based on step-climb logic.
1835
-
1836
- For short segments, the climb will be placed at the begining of the segment. For
1837
- long climbs (greater than two hours) the climb will be placed in the middle. For
1838
- all descents, the descent will be placed at the end of the segment.
1839
-
1840
- Parameters
1841
- ----------
1842
- altitude : npt.NDArray[np.float64]
1843
- Array of altitude values containing nan values. This function will raise
1844
- an error if ``altitude`` does not contain nan values. Moreover, this function
1845
- assumes the initial and final entries in ``altitude`` are not nan.
1846
- start_na_idxs : npt.NDArray[np.intp]
1847
- Array of indices of the array `altitude` that correspond to the last non-nan-
1848
- valued index before a sequence of consequtive nan values.
1849
- end_na_idxs : npt.NDArray[np.intp]
1850
- Array of indices of the array `altitude` that correspond to the first non-nan-
1851
- valued index after a sequence of consequtive nan values.
1852
- na_group_size : npt.NDArray[np.intp]
1853
- Array of the length of each consecutive sequence of nan values within the
1854
- array provided to input parameter `altitude`.
1855
- nominal_rocd : float
1856
- Nominal rate of climb/descent, in m/s
1857
- freq : np.timedelta64
1858
- Frequency of time index associated to ``altitude``.
1859
- isna : npt.NDArray[np.bool_]
1860
- Array of boolean values indicating whether or not each entry in `altitude`
1861
- is nan-valued.
1862
- -------
1863
- npt.NDArray[np.float64]
1864
- Altitude after nan values have been filled
1865
- """
1866
- s = pd.Series(altitude)
1867
-
1868
- # Check to see if we have gaps greater than two hours
1869
- step_threshold = np.timedelta64(2, "h") / freq
1870
- step_groups = na_group_size > step_threshold
1871
- if np.any(step_groups):
1872
- # If there are gaps greater than two hours, step through one by one
1873
- for i, step_group in enumerate(step_groups):
1874
- # Skip short segments and segments that do not climb
1875
- if not step_group:
1876
- continue
1877
- if s[start_na_idxs[i]] >= s[end_na_idxs[i]]:
1878
- continue
1879
-
1880
- # We have a long climbing segment. Keep first half of segment at the starting
1881
- # altitude, then climb at mid point. Adjust indicies computed before accordingly
1882
- na_group_size[i], is_odd = divmod(na_group_size[i], 2)
1883
- nan_fill_size = na_group_size[i] + is_odd
1884
-
1885
- sl = slice(start_na_idxs[i], start_na_idxs[i] + nan_fill_size + 1)
1886
- isna[sl] = False
1887
- s[sl] = s[start_na_idxs[i]]
1888
- start_na_idxs[i] += nan_fill_size
1889
-
1890
- # Use pandas to forward and backfill altitude values
1891
- s_ff = s.ffill()
1892
- s_bf = s.bfill()
1893
-
1894
- # Form array of cumulative altitude values if the flight were to climb
1895
- # at nominal_rocd over each group of nan
1896
- cumalt_list = []
1897
- for start_na_idx, end_na_idx, size in zip(
1898
- start_na_idxs, end_na_idxs, na_group_size, strict=True
1899
- ):
1900
- if s[start_na_idx] <= s[end_na_idx]:
1901
- cumalt_list.append(np.arange(1, size, dtype=float))
1902
- else:
1903
- cumalt_list.append(np.flip(np.arange(1, size, dtype=float)))
1904
-
1905
- cumalt = np.concatenate(cumalt_list)
1906
- cumalt = cumalt * nominal_rocd * (freq / np.timedelta64(1, "s"))
1907
-
1908
- # Expand cumalt to the full size of altitude
1909
- nominal_fill = np.zeros_like(altitude)
1910
- nominal_fill[isna] = cumalt
1911
-
1912
- # Construct altitude values if the flight were to climb / descent throughout
1913
- # group of consecutive nan values. The call to np.minimum / np.maximum cuts
1914
- # the climb / descent off at the terminal altitude of the nan group
1915
- fill_climb = np.minimum(s_ff + nominal_fill, s_bf)
1916
- fill_descent = np.minimum(s_ff, s_bf + nominal_fill)
1917
-
1918
- # Explicitly determine if the flight is in a climb or descent state
1919
- sign = np.full_like(altitude, np.nan)
1920
- sign[~isna] = np.sign(np.diff(s[~isna], append=np.nan))
1921
- sign = pd.Series(sign).ffill()
1786
+ # If unrealistic, assume flight will climb to cruise altitudes (0.8 * max_altitude_ft),
1787
+ # stay there, and then descent to the next known waypoint
1788
+ if is_unrealistic:
1789
+ # Add altitude at top of climb
1790
+ alt_ft_cruise = assumed_cruise_altitude_ft
1791
+ d_alt_start = alt_ft_cruise - alt_ft_start
1792
+ dt_climb = int(np.ceil(d_alt_start / nominal_rocd_ft_min))
1793
+ t_cruise_start = time_start + np.timedelta64(dt_climb, "m")
1794
+ idx_cruise_start = np.searchsorted(time, t_cruise_start) + 1
1795
+ alt_ft[idx_cruise_start] = alt_ft_cruise
1796
+
1797
+ # Add altitude at top of descent
1798
+ d_alt_end = alt_ft_cruise - alt_ft_end
1799
+ dt_descent = int(np.ceil(d_alt_end / nominal_rocd_ft_min))
1800
+ t_cruise_end = time_end - np.timedelta64(dt_descent, "m")
1801
+ idx_cruise_end = np.searchsorted(time, t_cruise_end) - 1
1802
+ alt_ft[idx_cruise_end] = alt_ft_cruise
1803
+ continue
1804
+
1805
+ # (2): If both altitudes are the same, then skip entire operations below
1806
+ if alt_ft_start == alt_ft_end:
1807
+ continue
1808
+
1809
+ # Rate of climb and descent to next waypoint, in ft/min
1810
+ rocd_next = (alt_ft_end - alt_ft_start) / dt_next
1811
+
1812
+ # (3): If cruise over 2 h with small altitude change, set change to mid-point
1813
+ is_long_segment_small_altitude_change = (
1814
+ dt_next > 120.0
1815
+ and rocd_next < 500.0
1816
+ and rocd_next > -500.0
1817
+ and alt_ft_start > minimum_cruise_altitude_ft
1818
+ and alt_ft_end > minimum_cruise_altitude_ft
1819
+ )
1922
1820
 
1923
- # And return the mess
1924
- return np.where(sign == 1.0, fill_climb, fill_descent)
1821
+ if is_long_segment_small_altitude_change:
1822
+ mid_na_idx = int(0.5 * (start_na_idxs[i] + end_na_idxs[i]))
1823
+ alt_ft[mid_na_idx] = alt_ft_start
1824
+ alt_ft[mid_na_idx + 1] = alt_ft_end
1825
+ continue
1826
+
1827
+ # (4): Climb at the start until target altitude and level off if:
1828
+ #: (i) large time gap (`dt_next` > 20 minutes) and positive `rocd`, or
1829
+ #: (ii) shallow climb (0 < `rocd_next` < 500 ft/min) between current and next waypoint
1830
+
1831
+ #: For (i), we only perform this for large time gaps, because we do not want the aircraft to
1832
+ #: constantly climb, level off, and repeat, while the ADS-B waypoints show that it is
1833
+ #: continuously climbing
1834
+ if (dt_next > 20.0 and rocd_next > 0.0) or (0.0 < rocd_next < 500.0):
1835
+ dt_climb = int(np.ceil((alt_ft_end - alt_ft_start) / nominal_rocd_ft_min * 60))
1836
+ t_climb_complete = time_start + np.timedelta64(dt_climb, "s")
1837
+ idx_climb_complete = np.searchsorted(time, t_climb_complete)
1838
+
1839
+ #: [Safeguard for very small `dt_next`] Ensure climb can be performed within the
1840
+ #: interpolated time step. If False, then aircraft will climb between waypoints instead
1841
+ #: of levelling off.
1842
+ if start_na_idxs[i] < idx_climb_complete < end_na_idxs[i]:
1843
+ alt_ft[idx_climb_complete] = alt_ft_end
1844
+
1845
+ continue
1846
+
1847
+ # (5): Descent towards the end until target altitude and level off if:
1848
+ #: (i) large time gap (`dt_next` > 20 minutes) and negative `rocd`, or
1849
+ #: (ii) shallow descent (-250 < `rocd_next` < 0 ft/min) between current and next waypoint
1850
+ if (dt_next > 20.0 and rocd_next < 0.0) or (-250.0 < rocd_next < 0.0):
1851
+ dt_descent = int(np.ceil((alt_ft_start - alt_ft_end) / nominal_rocd_ft_min * 60))
1852
+ t_descent_start = time_end - np.timedelta64(dt_descent, "s")
1853
+ idx_descent_start = np.where(
1854
+ -250.0 < rocd_next < 0.0,
1855
+ np.searchsorted(time, t_descent_start) - 1,
1856
+ np.searchsorted(time, t_descent_start),
1857
+ )
1925
1858
 
1859
+ #: [Safeguard for very small `dt_next`] Ensure descent can be performed within the
1860
+ #: interpolated time step. If False, then aircraft will descent between waypoints
1861
+ #: instead of levelling off.
1862
+ if start_na_idxs[i] < idx_descent_start < end_na_idxs[i]:
1863
+ alt_ft[idx_descent_start] = alt_ft_start
1926
1864
 
1927
- def _verify_altitude(
1928
- altitude: npt.NDArray[np.float64], nominal_rocd: float, freq: np.timedelta64
1929
- ) -> None:
1930
- """Confirm that the time derivative of `altitude` does not exceed twice `nominal_rocd`.
1865
+ continue
1931
1866
 
1932
- Parameters
1933
- ----------
1934
- altitude : npt.NDArray[np.float64]
1935
- Array of filled altitude values containing nan values.
1936
- nominal_rocd : float
1937
- Nominal rate of climb/descent, in m/s
1938
- freq : np.timedelta64
1939
- Frequency of time index associated to `altitude`.
1940
- """
1941
- dalt = np.diff(altitude)
1942
- dt = freq / np.timedelta64(1, "s")
1943
- rocd = np.abs(dalt / dt)
1944
- if np.any(rocd > 2.0 * nominal_rocd):
1945
- warnings.warn(
1946
- "Rate of climb/descent values greater than nominal "
1947
- f"({nominal_rocd} m/s) after altitude interpolation"
1948
- )
1949
- if np.any(np.isnan(altitude)):
1950
- warnings.warn(
1951
- f"Found nan values altitude after ({nominal_rocd} m/s) after altitude interpolation"
1952
- )
1867
+ # Linearly interpolate between remaining nan values
1868
+ out_alt_ft = pd.Series(alt_ft, index=time).interpolate(method="index")
1869
+ return units.ft_to_m(out_alt_ft.to_numpy())
1953
1870
 
1954
1871
 
1955
1872
  def filter_altitude(
1956
1873
  time: npt.NDArray[np.datetime64],
1957
- altitude_ft: npt.NDArray[np.float64],
1874
+ altitude_ft: npt.NDArray[np.floating],
1958
1875
  kernel_size: int = 17,
1959
1876
  cruise_threshold: float = 120,
1960
- air_temperature: None | npt.NDArray[np.float64] = None,
1961
- ) -> npt.NDArray[np.float64]:
1877
+ air_temperature: None | npt.NDArray[np.floating] = None,
1878
+ ) -> npt.NDArray[np.floating]:
1962
1879
  """
1963
1880
  Filter noisy altitude on a single flight.
1964
1881
 
@@ -1970,19 +1887,19 @@ def filter_altitude(
1970
1887
  ----------
1971
1888
  time : npt.NDArray[np.datetime64]
1972
1889
  Waypoint time in ``np.datetime64`` format.
1973
- altitude_ft : npt.NDArray[np.float64]
1890
+ altitude_ft : npt.NDArray[np.floating]
1974
1891
  Altitude signal in feet
1975
1892
  kernel_size : int, optional
1976
1893
  Passed directly to :func:`scipy.signal.medfilt`, by default 11.
1977
1894
  Passed also to :func:`scipy.signal.medfilt`
1978
1895
  cruise_theshold : int, optional
1979
1896
  Minimal length of time, in seconds, for a flight to be in cruise to apply median filter
1980
- air_temperature: None | npt.NDArray[np.float64]
1897
+ air_temperature: None | npt.NDArray[np.floating]
1981
1898
  Air temperature of each flight waypoint, [:math:`K`]
1982
1899
 
1983
1900
  Returns
1984
1901
  -------
1985
- npt.NDArray[np.float64]
1902
+ npt.NDArray[np.floating]
1986
1903
  Filtered altitude
1987
1904
 
1988
1905
  Notes
@@ -2072,7 +1989,7 @@ def filter_altitude(
2072
1989
 
2073
1990
  def segment_duration(
2074
1991
  time: npt.NDArray[np.datetime64], dtype: npt.DTypeLike = np.float32
2075
- ) -> npt.NDArray[np.float64]:
1992
+ ) -> npt.NDArray[np.floating]:
2076
1993
  """Calculate the time difference between waypoints.
2077
1994
 
2078
1995
  ``np.nan`` appended so the length of the output is the same as number of waypoints.
@@ -2083,13 +2000,13 @@ def segment_duration(
2083
2000
  Waypoint time in ``np.datetime64`` format.
2084
2001
  dtype : np.dtype
2085
2002
  Numpy dtype for time difference.
2086
- Defaults to ``np.float64``
2003
+ Defaults to ``np.float32``
2087
2004
 
2088
2005
  Returns
2089
2006
  -------
2090
- npt.NDArray[np.float64]
2007
+ npt.NDArray[np.floating]
2091
2008
  Time difference between waypoints, [:math:`s`].
2092
- This returns an array with dtype specified by``dtype``.
2009
+ This returns an array with dtype specified by ``dtype``.
2093
2010
  """
2094
2011
  out = np.empty_like(time, dtype=dtype)
2095
2012
  out[-1] = np.nan
@@ -2098,8 +2015,8 @@ def segment_duration(
2098
2015
 
2099
2016
 
2100
2017
  def segment_phase(
2101
- rocd: npt.NDArray[np.float64],
2102
- altitude_ft: npt.NDArray[np.float64],
2018
+ rocd: npt.NDArray[np.floating],
2019
+ altitude_ft: npt.NDArray[np.floating],
2103
2020
  *,
2104
2021
  threshold_rocd: float = 250.0,
2105
2022
  min_cruise_altitude_ft: float = MIN_CRUISE_ALTITUDE,
@@ -2111,7 +2028,7 @@ def segment_phase(
2111
2028
  rocd: pt.NDArray[np.float64]
2112
2029
  Rate of climb and descent across segment, [:math:`ft min^{-1}`].
2113
2030
  See output from :func:`segment_rocd`.
2114
- altitude_ft: npt.NDArray[np.float64]
2031
+ altitude_ft: npt.NDArray[np.floating]
2115
2032
  Altitude, [:math:`ft`]
2116
2033
  threshold_rocd: float, optional
2117
2034
  ROCD threshold to identify climb and descent, [:math:`ft min^{-1}`].
@@ -2161,31 +2078,31 @@ def segment_phase(
2161
2078
 
2162
2079
 
2163
2080
  def segment_rocd(
2164
- segment_duration: npt.NDArray[np.float64],
2165
- altitude_ft: npt.NDArray[np.float64],
2166
- air_temperature: None | npt.NDArray[np.float64] = None,
2167
- ) -> npt.NDArray[np.float64]:
2081
+ segment_duration: npt.NDArray[np.floating],
2082
+ altitude_ft: npt.NDArray[np.floating],
2083
+ air_temperature: npt.NDArray[np.floating] | None = None,
2084
+ ) -> npt.NDArray[np.floating]:
2168
2085
  """Calculate the rate of climb and descent (ROCD).
2169
2086
 
2170
2087
  Parameters
2171
2088
  ----------
2172
- segment_duration: npt.NDArray[np.float64]
2089
+ segment_duration: npt.NDArray[np.floating]
2173
2090
  Time difference between waypoints, [:math:`s`].
2174
- Expected to have numeric `dtype`, not `"timedelta64"`.
2091
+ Expected to have numeric ``dtype``, not ``np.timedelta64``.
2175
2092
  See output from :func:`segment_duration`.
2176
- altitude_ft: npt.NDArray[np.float64]
2093
+ altitude_ft: npt.NDArray[np.floating]
2177
2094
  Altitude of each waypoint, [:math:`ft`]
2178
- air_temperature: None | npt.NDArray[np.float64]
2095
+ air_temperature: npt.NDArray[np.floating] | None
2179
2096
  Air temperature of each flight waypoint, [:math:`K`]
2180
2097
 
2181
2098
  Returns
2182
2099
  -------
2183
- npt.NDArray[np.float64]
2100
+ npt.NDArray[np.floating]
2184
2101
  Rate of climb and descent over segment, [:math:`ft min^{-1}`]
2185
2102
 
2186
2103
  Notes
2187
2104
  -----
2188
- The hydrostatic equation will be used to estimate the ROCD if `air_temperature` is provided.
2105
+ The hydrostatic equation will be used to estimate the ROCD if ``air_temperature`` is provided.
2189
2106
  This will improve the accuracy of the estimated ROCD with a temperature correction. The
2190
2107
  estimated ROCD with the temperature correction are expected to differ by up to +-5% compared to
2191
2108
  those without the correction. These differences are important when the ROCD estimates are used
@@ -2193,7 +2110,7 @@ def segment_rocd(
2193
2110
 
2194
2111
  See Also
2195
2112
  --------
2196
- :func:`segment_duration`
2113
+ segment_duration
2197
2114
  """
2198
2115
  dt_min = segment_duration / 60.0
2199
2116
 
@@ -2211,7 +2128,7 @@ def segment_rocd(
2211
2128
  T_correction[:-1] = (air_temperature[:-1] + air_temperature[1:]) / (T_isa[:-1] + T_isa[1:])
2212
2129
  T_correction[-1] = np.nan
2213
2130
 
2214
- return T_correction * out
2131
+ return T_correction * out # type: ignore[return-value]
2215
2132
 
2216
2133
 
2217
2134
  def _resample_to_freq(df: pd.DataFrame, freq: str) -> tuple[pd.DataFrame, pd.DatetimeIndex]: