pycontrails 0.54.3__cp313-cp313-macosx_11_0_arm64.whl → 0.54.4__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 (60) 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 +25 -21
  7. pycontrails/core/flight.py +213 -301
  8. pycontrails/core/interpolation.py +56 -56
  9. pycontrails/core/met.py +48 -39
  10. pycontrails/core/models.py +25 -11
  11. pycontrails/core/polygon.py +15 -15
  12. pycontrails/core/rgi_cython.cpython-313-darwin.so +0 -0
  13. pycontrails/core/vector.py +22 -22
  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 +3 -3
  25. pycontrails/models/cocip/__init__.py +2 -2
  26. pycontrails/models/cocip/cocip.py +15 -15
  27. pycontrails/models/cocip/cocip_params.py +2 -11
  28. pycontrails/models/cocip/cocip_uncertainty.py +24 -18
  29. pycontrails/models/cocip/contrail_properties.py +331 -316
  30. pycontrails/models/cocip/output_formats.py +53 -53
  31. pycontrails/models/cocip/radiative_forcing.py +135 -131
  32. pycontrails/models/cocip/radiative_heating.py +135 -135
  33. pycontrails/models/cocip/unterstrasser_wake_vortex.py +90 -87
  34. pycontrails/models/cocip/wake_vortex.py +92 -92
  35. pycontrails/models/cocip/wind_shear.py +8 -8
  36. pycontrails/models/cocipgrid/cocip_grid.py +93 -87
  37. pycontrails/models/dry_advection.py +10 -5
  38. pycontrails/models/emissions/__init__.py +2 -2
  39. pycontrails/models/emissions/black_carbon.py +108 -108
  40. pycontrails/models/emissions/emissions.py +85 -85
  41. pycontrails/models/emissions/ffm2.py +35 -35
  42. pycontrails/models/humidity_scaling/humidity_scaling.py +23 -23
  43. pycontrails/models/ps_model/__init__.py +1 -1
  44. pycontrails/models/ps_model/ps_aircraft_params.py +8 -4
  45. pycontrails/models/ps_model/ps_grid.py +74 -64
  46. pycontrails/models/ps_model/ps_model.py +14 -14
  47. pycontrails/models/ps_model/ps_operational_limits.py +20 -18
  48. pycontrails/models/tau_cirrus.py +8 -1
  49. pycontrails/physics/geo.py +67 -67
  50. pycontrails/physics/jet.py +79 -79
  51. pycontrails/physics/units.py +14 -14
  52. pycontrails/utils/json.py +1 -2
  53. pycontrails/utils/types.py +12 -7
  54. {pycontrails-0.54.3.dist-info → pycontrails-0.54.4.dist-info}/METADATA +2 -2
  55. {pycontrails-0.54.3.dist-info → pycontrails-0.54.4.dist-info}/NOTICE +1 -1
  56. pycontrails-0.54.4.dist-info/RECORD +111 -0
  57. pycontrails-0.54.3.dist-info/RECORD +0 -111
  58. {pycontrails-0.54.3.dist-info → pycontrails-0.54.4.dist-info}/LICENSE +0 -0
  59. {pycontrails-0.54.3.dist-info → pycontrails-0.54.4.dist-info}/WHEEL +0 -0
  60. {pycontrails-0.54.3.dist-info → pycontrails-0.54.4.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,7 @@ 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
+ return type(self)(data=df, attrs=self.attrs, fuel=self.fuel)
967
961
 
968
962
  def clean_and_resample(
969
963
  self,
@@ -976,8 +970,7 @@ class Flight(GeoVectorDataset):
976
970
  force_filter: bool = False,
977
971
  drop: bool = True,
978
972
  keep_original_index: bool = False,
979
- climb_descend_at_end: bool = False,
980
- ) -> Flight:
973
+ ) -> Self:
981
974
  """Resample and (possibly) filter a flight trajectory.
982
975
 
983
976
  Waypoints are resampled according to the frequency ``freq``. If the original
@@ -1018,9 +1011,6 @@ class Flight(GeoVectorDataset):
1018
1011
  Keep the original index of the :class:`Flight` in addition to the new
1019
1012
  resampled index. Defaults to ``False``.
1020
1013
  .. 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
1014
 
1025
1015
  Returns
1026
1016
  -------
@@ -1039,7 +1029,6 @@ class Flight(GeoVectorDataset):
1039
1029
  nominal_rocd,
1040
1030
  drop,
1041
1031
  keep_original_index,
1042
- climb_descend_at_end,
1043
1032
  )
1044
1033
 
1045
1034
  # If the flight has large gap(s), then call resample and fill, then filter altitude
@@ -1055,7 +1044,6 @@ class Flight(GeoVectorDataset):
1055
1044
  nominal_rocd,
1056
1045
  drop,
1057
1046
  keep_original_index,
1058
- climb_descend_at_end,
1059
1047
  )
1060
1048
  clean_flight = clean_flight.filter_altitude(kernel_size, cruise_threshold)
1061
1049
  else:
@@ -1069,14 +1057,13 @@ class Flight(GeoVectorDataset):
1069
1057
  nominal_rocd,
1070
1058
  drop,
1071
1059
  keep_original_index,
1072
- climb_descend_at_end,
1073
1060
  )
1074
1061
 
1075
1062
  def filter_altitude(
1076
1063
  self,
1077
1064
  kernel_size: int = 17,
1078
1065
  cruise_threshold: float = 120.0,
1079
- ) -> Flight:
1066
+ ) -> Self:
1080
1067
  """
1081
1068
  Filter noisy altitude on a single flight.
1082
1069
 
@@ -1508,7 +1495,7 @@ class Flight(GeoVectorDataset):
1508
1495
  >>> fl["air_temperature"] = fl.intersect_met(met["air_temperature"])
1509
1496
  >>> fl["air_temperature"]
1510
1497
  array([235.94657007, 235.55745645, 235.56709768, ..., 234.59917962,
1511
- 234.60387402, 234.60845312])
1498
+ 234.60387402, 234.60845312], shape=(1081,))
1512
1499
 
1513
1500
  >>> # Length (in meters) of waypoints whose temperature exceeds 236K
1514
1501
  >>> fl.length_met("air_temperature", threshold=236)
@@ -1606,12 +1593,12 @@ class Flight(GeoVectorDataset):
1606
1593
  return ax
1607
1594
 
1608
1595
 
1609
- def _return_linestring(data: dict[str, npt.NDArray[np.float64]]) -> list[list[float]]:
1596
+ def _return_linestring(data: dict[str, npt.NDArray[np.floating]]) -> list[list[float]]:
1610
1597
  """Return list of coordinates for geojson constructions.
1611
1598
 
1612
1599
  Parameters
1613
1600
  ----------
1614
- data : dict[str, npt.NDArray[np.float64]]
1601
+ data : dict[str, npt.NDArray[np.floating]]
1615
1602
  :attr:`data` containing `longitude`, `latitude`, and `altitude` keys
1616
1603
 
1617
1604
  Returns
@@ -1659,15 +1646,15 @@ def _antimeridian_index(longitude: pd.Series) -> list[int]:
1659
1646
 
1660
1647
 
1661
1648
  def _sg_filter(
1662
- vals: npt.NDArray[np.float64], window_length: int = 7, polyorder: int = 1
1663
- ) -> npt.NDArray[np.float64]:
1649
+ vals: npt.NDArray[np.floating], window_length: int = 7, polyorder: int = 1
1650
+ ) -> npt.NDArray[np.floating]:
1664
1651
  """Apply Savitzky-Golay filter to smooth out noise in the time-series data.
1665
1652
 
1666
1653
  Used to smooth true airspeed, fuel flow, and altitude.
1667
1654
 
1668
1655
  Parameters
1669
1656
  ----------
1670
- vals : npt.NDArray[np.float64]
1657
+ vals : npt.NDArray[np.floating]
1671
1658
  Input array
1672
1659
  window_length : int, optional
1673
1660
  Parameter for :func:`scipy.signal.savgol_filter`
@@ -1676,7 +1663,7 @@ def _sg_filter(
1676
1663
 
1677
1664
  Returns
1678
1665
  -------
1679
- npt.NDArray[np.float64]
1666
+ npt.NDArray[np.floating]
1680
1667
  Smoothed values
1681
1668
 
1682
1669
  Raises
@@ -1702,11 +1689,12 @@ def _sg_filter(
1702
1689
 
1703
1690
 
1704
1691
  def _altitude_interpolation(
1705
- altitude: npt.NDArray[np.float64],
1692
+ altitude: npt.NDArray[np.floating],
1693
+ time: npt.NDArray[np.datetime64],
1706
1694
  nominal_rocd: float,
1707
- freq: np.timedelta64,
1708
- climb_or_descend_at_end: bool = False,
1709
- ) -> npt.NDArray[np.float64]:
1695
+ minimum_cruise_altitude_ft: float = 20000.0,
1696
+ assumed_cruise_altitude_ft: float = 30000.0,
1697
+ ) -> npt.NDArray[np.floating]:
1710
1698
  """Interpolate nan values in ``altitude`` array.
1711
1699
 
1712
1700
  Suppose each group of consecutive nan values is enclosed by ``a0`` and ``a1`` with
@@ -1718,31 +1706,50 @@ def _altitude_interpolation(
1718
1706
 
1719
1707
  Parameters
1720
1708
  ----------
1721
- altitude : npt.NDArray[np.float64]
1709
+ altitude : npt.NDArray[np.floating]
1722
1710
  Array of altitude values containing nan values. This function will raise
1723
1711
  an error if ``altitude`` does not contain nan values. Moreover, this function
1724
- assumes the initial and final entries in ``altitude`` are not nan.
1712
+ assumes the initial and final entries in ``altitude`` are not nan, [:math:`m`]
1713
+ time : npt.NDArray[np.datetime64]
1714
+ Timestamp at each waypoint. Must be monotonically increasing.
1725
1715
  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).
1716
+ Nominal rate of climb/descent, [:math:`m s^{-1}`]
1717
+ minimum_cruise_altitude_ft : float
1718
+ Minimium cruising altitude for a given aircraft type, [:math:`ft`].
1719
+ By default, this is 20000.0 ft.
1720
+ assumed_cruise_altitude_ft : float
1721
+ Assumed cruising altitude for a given aircraft type, [:math:`ft`].
1722
+ By default, this is 30000.0 ft.
1732
1723
 
1733
1724
  Returns
1734
1725
  -------
1735
- npt.NDArray[np.float64]
1736
- Altitude after nan values have been filled
1726
+ npt.NDArray[np.floating]
1727
+ Altitude after nan values have been filled, [:math:`m`]
1728
+
1729
+ Notes
1730
+ -----
1731
+ Default values for ``minimum_cruise_altitude_ft`` and ``assumed_cruise_altitude_ft`` should be
1732
+ provided if aircraft-specific parameters are available to improve the output quality.
1733
+
1734
+ We can assume ``minimum_cruise_altitude_ft`` as 0.5 times the aircraft service ceiling, and
1735
+ ``assumed_cruise_altitude_ft`` as 0.8 times the aircraft service ceiling.
1736
+
1737
+ Assume that aircraft will generally prefer to climb to a higher altitude as early as possible,
1738
+ and descent to a lower altitude as late as possible, because a higher altitude can reduce
1739
+ drag and fuel consumption.
1737
1740
  """
1741
+ # Work in units of feet
1742
+ alt_ft = units.m_to_ft(altitude)
1743
+ nominal_rocd_ft_min = units.m_to_ft(nominal_rocd) * 60.0
1744
+
1738
1745
  # Determine nan state of altitude
1739
- isna = np.isnan(altitude)
1746
+ isna = np.isnan(alt_ft)
1740
1747
 
1741
- start_na = np.empty(altitude.size, dtype=bool)
1748
+ start_na = np.empty(alt_ft.size, dtype=bool)
1742
1749
  start_na[:-1] = ~isna[:-1] & isna[1:]
1743
1750
  start_na[-1] = False
1744
1751
 
1745
- end_na = np.empty(altitude.size, dtype=bool)
1752
+ end_na = np.empty(alt_ft.size, dtype=bool)
1746
1753
  end_na[0] = False
1747
1754
  end_na[1:] = isna[:-1] & ~isna[1:]
1748
1755
 
@@ -1751,214 +1758,119 @@ def _altitude_interpolation(
1751
1758
  end_na_idxs = np.flatnonzero(end_na)
1752
1759
  na_group_size = end_na_idxs - start_na_idxs
1753
1760
 
1754
- if climb_or_descend_at_end:
1755
- return _altitude_interpolation_climb_descend_end(
1756
- altitude, na_group_size, nominal_rocd, freq, isna
1761
+ # NOTE: Only fill altitude gaps that require special attention
1762
+ # At the end of this for loop, those with NaN altitudes will be filled with pd.interpolate
1763
+ for i in range(len(na_group_size)):
1764
+ alt_ft_start = alt_ft[start_na_idxs[i]]
1765
+ alt_ft_end = alt_ft[end_na_idxs[i]]
1766
+ time_start = time[start_na_idxs[i]]
1767
+ time_end = time[end_na_idxs[i]]
1768
+
1769
+ # Calculate parameters to determine how to interpolate altitude
1770
+ # Time to next waypoint
1771
+ dt_next = (time_end - time_start) / np.timedelta64(1, "m")
1772
+
1773
+ # (1): Unrealistic scenario: first and next known waypoints are at very
1774
+ # low altitudes with a large time gap.
1775
+ is_unrealistic = (
1776
+ dt_next > 60.0
1777
+ and alt_ft_start < minimum_cruise_altitude_ft
1778
+ and alt_ft_end < minimum_cruise_altitude_ft
1757
1779
  )
1758
1780
 
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()
1781
+ # If unrealistic, assume flight will climb to cruise altitudes (0.8 * max_altitude_ft),
1782
+ # stay there, and then descent to the next known waypoint
1783
+ if is_unrealistic:
1784
+ # Add altitude at top of climb
1785
+ alt_ft_cruise = assumed_cruise_altitude_ft
1786
+ d_alt_start = alt_ft_cruise - alt_ft_start
1787
+ dt_climb = int(np.ceil(d_alt_start / nominal_rocd_ft_min))
1788
+ t_cruise_start = time_start + np.timedelta64(dt_climb, "m")
1789
+ idx_cruise_start = np.searchsorted(time, t_cruise_start) + 1
1790
+ alt_ft[idx_cruise_start] = alt_ft_cruise
1791
+
1792
+ # Add altitude at top of descent
1793
+ d_alt_end = alt_ft_cruise - alt_ft_end
1794
+ dt_descent = int(np.ceil(d_alt_end / nominal_rocd_ft_min))
1795
+ t_cruise_end = time_end - np.timedelta64(dt_descent, "m")
1796
+ idx_cruise_end = np.searchsorted(time, t_cruise_end) - 1
1797
+ alt_ft[idx_cruise_end] = alt_ft_cruise
1798
+ continue
1799
+
1800
+ # (2): If both altitudes are the same, then skip entire operations below
1801
+ if alt_ft_start == alt_ft_end:
1802
+ continue
1803
+
1804
+ # Rate of climb and descent to next waypoint, in ft/min
1805
+ rocd_next = (alt_ft_end - alt_ft_start) / dt_next
1806
+
1807
+ # (3): If cruise over 2 h with small altitude change, set change to mid-point
1808
+ is_long_segment_small_altitude_change = (
1809
+ dt_next > 120.0
1810
+ and rocd_next < 500.0
1811
+ and rocd_next > -500.0
1812
+ and alt_ft_start > minimum_cruise_altitude_ft
1813
+ and alt_ft_end > minimum_cruise_altitude_ft
1814
+ )
1922
1815
 
1923
- # And return the mess
1924
- return np.where(sign == 1.0, fill_climb, fill_descent)
1816
+ if is_long_segment_small_altitude_change:
1817
+ mid_na_idx = int(0.5 * (start_na_idxs[i] + end_na_idxs[i]))
1818
+ alt_ft[mid_na_idx] = alt_ft_start
1819
+ alt_ft[mid_na_idx + 1] = alt_ft_end
1820
+ continue
1821
+
1822
+ # (4): Climb at the start until target altitude and level off if:
1823
+ #: (i) large time gap (`dt_next` > 20 minutes) and positive `rocd`, or
1824
+ #: (ii) shallow climb (0 < `rocd_next` < 500 ft/min) between current and next waypoint
1825
+
1826
+ #: For (i), we only perform this for large time gaps, because we do not want the aircraft to
1827
+ #: constantly climb, level off, and repeat, while the ADS-B waypoints show that it is
1828
+ #: continuously climbing
1829
+ if (dt_next > 20.0 and rocd_next > 0.0) or (0.0 < rocd_next < 500.0):
1830
+ dt_climb = int(np.ceil((alt_ft_end - alt_ft_start) / nominal_rocd_ft_min * 60))
1831
+ t_climb_complete = time_start + np.timedelta64(dt_climb, "s")
1832
+ idx_climb_complete = np.searchsorted(time, t_climb_complete)
1833
+
1834
+ #: [Safeguard for very small `dt_next`] Ensure climb can be performed within the
1835
+ #: interpolated time step. If False, then aircraft will climb between waypoints instead
1836
+ #: of levelling off.
1837
+ if start_na_idxs[i] < idx_climb_complete < end_na_idxs[i]:
1838
+ alt_ft[idx_climb_complete] = alt_ft_end
1839
+
1840
+ continue
1841
+
1842
+ # (5): Descent towards the end until target altitude and level off if:
1843
+ #: (i) large time gap (`dt_next` > 20 minutes) and negative `rocd`, or
1844
+ #: (ii) shallow descent (-250 < `rocd_next` < 0 ft/min) between current and next waypoint
1845
+ if (dt_next > 20.0 and rocd_next < 0.0) or (-250.0 < rocd_next < 0.0):
1846
+ dt_descent = int(np.ceil((alt_ft_start - alt_ft_end) / nominal_rocd_ft_min * 60))
1847
+ t_descent_start = time_end - np.timedelta64(dt_descent, "s")
1848
+ idx_descent_start = np.where(
1849
+ -250.0 < rocd_next < 0.0,
1850
+ np.searchsorted(time, t_descent_start) - 1,
1851
+ np.searchsorted(time, t_descent_start),
1852
+ )
1925
1853
 
1854
+ #: [Safeguard for very small `dt_next`] Ensure descent can be performed within the
1855
+ #: interpolated time step. If False, then aircraft will descent between waypoints
1856
+ #: instead of levelling off.
1857
+ if start_na_idxs[i] < idx_descent_start < end_na_idxs[i]:
1858
+ alt_ft[idx_descent_start] = alt_ft_start
1926
1859
 
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`.
1860
+ continue
1931
1861
 
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
- )
1862
+ # Linearly interpolate between remaining nan values
1863
+ out_alt_ft = pd.Series(alt_ft, index=time).interpolate(method="index")
1864
+ return units.ft_to_m(out_alt_ft.to_numpy())
1953
1865
 
1954
1866
 
1955
1867
  def filter_altitude(
1956
1868
  time: npt.NDArray[np.datetime64],
1957
- altitude_ft: npt.NDArray[np.float64],
1869
+ altitude_ft: npt.NDArray[np.floating],
1958
1870
  kernel_size: int = 17,
1959
1871
  cruise_threshold: float = 120,
1960
- air_temperature: None | npt.NDArray[np.float64] = None,
1961
- ) -> npt.NDArray[np.float64]:
1872
+ air_temperature: None | npt.NDArray[np.floating] = None,
1873
+ ) -> npt.NDArray[np.floating]:
1962
1874
  """
1963
1875
  Filter noisy altitude on a single flight.
1964
1876
 
@@ -1970,19 +1882,19 @@ def filter_altitude(
1970
1882
  ----------
1971
1883
  time : npt.NDArray[np.datetime64]
1972
1884
  Waypoint time in ``np.datetime64`` format.
1973
- altitude_ft : npt.NDArray[np.float64]
1885
+ altitude_ft : npt.NDArray[np.floating]
1974
1886
  Altitude signal in feet
1975
1887
  kernel_size : int, optional
1976
1888
  Passed directly to :func:`scipy.signal.medfilt`, by default 11.
1977
1889
  Passed also to :func:`scipy.signal.medfilt`
1978
1890
  cruise_theshold : int, optional
1979
1891
  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]
1892
+ air_temperature: None | npt.NDArray[np.floating]
1981
1893
  Air temperature of each flight waypoint, [:math:`K`]
1982
1894
 
1983
1895
  Returns
1984
1896
  -------
1985
- npt.NDArray[np.float64]
1897
+ npt.NDArray[np.floating]
1986
1898
  Filtered altitude
1987
1899
 
1988
1900
  Notes
@@ -2072,7 +1984,7 @@ def filter_altitude(
2072
1984
 
2073
1985
  def segment_duration(
2074
1986
  time: npt.NDArray[np.datetime64], dtype: npt.DTypeLike = np.float32
2075
- ) -> npt.NDArray[np.float64]:
1987
+ ) -> npt.NDArray[np.floating]:
2076
1988
  """Calculate the time difference between waypoints.
2077
1989
 
2078
1990
  ``np.nan`` appended so the length of the output is the same as number of waypoints.
@@ -2083,13 +1995,13 @@ def segment_duration(
2083
1995
  Waypoint time in ``np.datetime64`` format.
2084
1996
  dtype : np.dtype
2085
1997
  Numpy dtype for time difference.
2086
- Defaults to ``np.float64``
1998
+ Defaults to ``np.float32``
2087
1999
 
2088
2000
  Returns
2089
2001
  -------
2090
- npt.NDArray[np.float64]
2002
+ npt.NDArray[np.floating]
2091
2003
  Time difference between waypoints, [:math:`s`].
2092
- This returns an array with dtype specified by``dtype``.
2004
+ This returns an array with dtype specified by ``dtype``.
2093
2005
  """
2094
2006
  out = np.empty_like(time, dtype=dtype)
2095
2007
  out[-1] = np.nan
@@ -2098,8 +2010,8 @@ def segment_duration(
2098
2010
 
2099
2011
 
2100
2012
  def segment_phase(
2101
- rocd: npt.NDArray[np.float64],
2102
- altitude_ft: npt.NDArray[np.float64],
2013
+ rocd: npt.NDArray[np.floating],
2014
+ altitude_ft: npt.NDArray[np.floating],
2103
2015
  *,
2104
2016
  threshold_rocd: float = 250.0,
2105
2017
  min_cruise_altitude_ft: float = MIN_CRUISE_ALTITUDE,
@@ -2111,7 +2023,7 @@ def segment_phase(
2111
2023
  rocd: pt.NDArray[np.float64]
2112
2024
  Rate of climb and descent across segment, [:math:`ft min^{-1}`].
2113
2025
  See output from :func:`segment_rocd`.
2114
- altitude_ft: npt.NDArray[np.float64]
2026
+ altitude_ft: npt.NDArray[np.floating]
2115
2027
  Altitude, [:math:`ft`]
2116
2028
  threshold_rocd: float, optional
2117
2029
  ROCD threshold to identify climb and descent, [:math:`ft min^{-1}`].
@@ -2161,31 +2073,31 @@ def segment_phase(
2161
2073
 
2162
2074
 
2163
2075
  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]:
2076
+ segment_duration: npt.NDArray[np.floating],
2077
+ altitude_ft: npt.NDArray[np.floating],
2078
+ air_temperature: npt.NDArray[np.floating] | None = None,
2079
+ ) -> npt.NDArray[np.floating]:
2168
2080
  """Calculate the rate of climb and descent (ROCD).
2169
2081
 
2170
2082
  Parameters
2171
2083
  ----------
2172
- segment_duration: npt.NDArray[np.float64]
2084
+ segment_duration: npt.NDArray[np.floating]
2173
2085
  Time difference between waypoints, [:math:`s`].
2174
- Expected to have numeric `dtype`, not `"timedelta64"`.
2086
+ Expected to have numeric ``dtype``, not ``np.timedelta64``.
2175
2087
  See output from :func:`segment_duration`.
2176
- altitude_ft: npt.NDArray[np.float64]
2088
+ altitude_ft: npt.NDArray[np.floating]
2177
2089
  Altitude of each waypoint, [:math:`ft`]
2178
- air_temperature: None | npt.NDArray[np.float64]
2090
+ air_temperature: npt.NDArray[np.floating] | None
2179
2091
  Air temperature of each flight waypoint, [:math:`K`]
2180
2092
 
2181
2093
  Returns
2182
2094
  -------
2183
- npt.NDArray[np.float64]
2095
+ npt.NDArray[np.floating]
2184
2096
  Rate of climb and descent over segment, [:math:`ft min^{-1}`]
2185
2097
 
2186
2098
  Notes
2187
2099
  -----
2188
- The hydrostatic equation will be used to estimate the ROCD if `air_temperature` is provided.
2100
+ The hydrostatic equation will be used to estimate the ROCD if ``air_temperature`` is provided.
2189
2101
  This will improve the accuracy of the estimated ROCD with a temperature correction. The
2190
2102
  estimated ROCD with the temperature correction are expected to differ by up to +-5% compared to
2191
2103
  those without the correction. These differences are important when the ROCD estimates are used
@@ -2193,7 +2105,7 @@ def segment_rocd(
2193
2105
 
2194
2106
  See Also
2195
2107
  --------
2196
- :func:`segment_duration`
2108
+ segment_duration
2197
2109
  """
2198
2110
  dt_min = segment_duration / 60.0
2199
2111
 
@@ -2211,7 +2123,7 @@ def segment_rocd(
2211
2123
  T_correction[:-1] = (air_temperature[:-1] + air_temperature[1:]) / (T_isa[:-1] + T_isa[1:])
2212
2124
  T_correction[-1] = np.nan
2213
2125
 
2214
- return T_correction * out
2126
+ return T_correction * out # type: ignore[return-value]
2215
2127
 
2216
2128
 
2217
2129
  def _resample_to_freq(df: pd.DataFrame, freq: str) -> tuple[pd.DataFrame, pd.DatetimeIndex]: