pycontrails 0.54.2__cp310-cp310-macosx_11_0_arm64.whl → 0.54.4__cp310-cp310-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 (68) 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 +75 -61
  5. pycontrails/core/cache.py +7 -7
  6. pycontrails/core/fleet.py +25 -21
  7. pycontrails/core/flight.py +215 -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-310-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/ecmwf/variables.py +1 -0
  21. pycontrails/datalib/gfs/__init__.py +6 -6
  22. pycontrails/datalib/gfs/gfs.py +2 -2
  23. pycontrails/datalib/goes.py +5 -5
  24. pycontrails/datalib/landsat.py +5 -8
  25. pycontrails/datalib/sentinel.py +7 -11
  26. pycontrails/ext/bada.py +3 -2
  27. pycontrails/ext/empirical_grid.py +1 -1
  28. pycontrails/ext/synthetic_flight.py +3 -2
  29. pycontrails/models/accf.py +40 -19
  30. pycontrails/models/apcemm/apcemm.py +5 -4
  31. pycontrails/models/cocip/__init__.py +2 -2
  32. pycontrails/models/cocip/cocip.py +16 -17
  33. pycontrails/models/cocip/cocip_params.py +2 -11
  34. pycontrails/models/cocip/cocip_uncertainty.py +24 -18
  35. pycontrails/models/cocip/contrail_properties.py +331 -316
  36. pycontrails/models/cocip/output_formats.py +53 -53
  37. pycontrails/models/cocip/radiative_forcing.py +135 -131
  38. pycontrails/models/cocip/radiative_heating.py +135 -135
  39. pycontrails/models/cocip/unterstrasser_wake_vortex.py +90 -87
  40. pycontrails/models/cocip/wake_vortex.py +92 -92
  41. pycontrails/models/cocip/wind_shear.py +8 -8
  42. pycontrails/models/cocipgrid/cocip_grid.py +118 -107
  43. pycontrails/models/dry_advection.py +59 -58
  44. pycontrails/models/emissions/__init__.py +2 -2
  45. pycontrails/models/emissions/black_carbon.py +108 -108
  46. pycontrails/models/emissions/emissions.py +85 -85
  47. pycontrails/models/emissions/ffm2.py +35 -35
  48. pycontrails/models/humidity_scaling/humidity_scaling.py +23 -23
  49. pycontrails/models/ps_model/__init__.py +3 -2
  50. pycontrails/models/ps_model/ps_aircraft_params.py +11 -6
  51. pycontrails/models/ps_model/ps_grid.py +256 -60
  52. pycontrails/models/ps_model/ps_model.py +18 -21
  53. pycontrails/models/ps_model/ps_operational_limits.py +58 -69
  54. pycontrails/models/tau_cirrus.py +8 -1
  55. pycontrails/physics/geo.py +216 -67
  56. pycontrails/physics/jet.py +220 -90
  57. pycontrails/physics/static/iata-cargo-load-factors-20241115.csv +71 -0
  58. pycontrails/physics/static/iata-passenger-load-factors-20241115.csv +71 -0
  59. pycontrails/physics/units.py +14 -14
  60. pycontrails/utils/json.py +1 -2
  61. pycontrails/utils/types.py +12 -7
  62. {pycontrails-0.54.2.dist-info → pycontrails-0.54.4.dist-info}/METADATA +10 -10
  63. {pycontrails-0.54.2.dist-info → pycontrails-0.54.4.dist-info}/NOTICE +1 -1
  64. pycontrails-0.54.4.dist-info/RECORD +111 -0
  65. {pycontrails-0.54.2.dist-info → pycontrails-0.54.4.dist-info}/WHEEL +1 -1
  66. pycontrails-0.54.2.dist-info/RECORD +0 -109
  67. {pycontrails-0.54.2.dist-info → pycontrails-0.54.4.dist-info}/LICENSE +0 -0
  68. {pycontrails-0.54.2.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
@@ -121,6 +124,8 @@ class Flight(GeoVectorDataset):
121
124
  calculations with the ICAO Aircraft Emissions Databank (EDB).
122
125
  - ``max_mach_number``: Maximum Mach number at cruise altitude. Used by
123
126
  some aircraft performance models to clip true airspeed.
127
+ - ``load_factor``: The load factor used in determining the aircraft's
128
+ take-off weight. Used by some aircraft performance models.
124
129
 
125
130
  Numeric quantities that are constant over the entire flight trajectory
126
131
  should be included as attributes.
@@ -283,14 +288,12 @@ class Flight(GeoVectorDataset):
283
288
  )
284
289
 
285
290
  @override
286
- def copy(self: FlightType, **kwargs: Any) -> FlightType:
291
+ def copy(self, **kwargs: Any) -> Self:
287
292
  kwargs.setdefault("fuel", self.fuel)
288
293
  return super().copy(**kwargs)
289
294
 
290
295
  @override
291
- def filter(
292
- self: FlightType, mask: npt.NDArray[np.bool_], copy: bool = True, **kwargs: Any
293
- ) -> FlightType:
296
+ def filter(self, mask: npt.NDArray[np.bool_], copy: bool = True, **kwargs: Any) -> Self:
294
297
  kwargs.setdefault("fuel", self.fuel)
295
298
  return super().filter(mask, copy=copy, **kwargs)
296
299
 
@@ -412,7 +415,7 @@ class Flight(GeoVectorDataset):
412
415
  # Segment Properties
413
416
  # ------------
414
417
 
415
- 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]:
416
419
  r"""Compute time elapsed between waypoints in seconds.
417
420
 
418
421
  ``np.nan`` appended so the length of the output is the same as number of waypoints.
@@ -425,14 +428,14 @@ class Flight(GeoVectorDataset):
425
428
 
426
429
  Returns
427
430
  -------
428
- npt.NDArray[np.float64]
431
+ npt.NDArray[np.floating]
429
432
  Time difference between waypoints, [:math:`s`].
430
433
  Returns an array with dtype specified by``dtype``
431
434
  """
432
435
 
433
436
  return segment_duration(self.data["time"], dtype=dtype)
434
437
 
435
- def segment_haversine(self) -> npt.NDArray[np.float64]:
438
+ def segment_haversine(self) -> npt.NDArray[np.floating]:
436
439
  """Compute Haversine (great circle) distance between flight waypoints.
437
440
 
438
441
  Helper function used in :meth:`resample_and_fill`.
@@ -443,7 +446,7 @@ class Flight(GeoVectorDataset):
443
446
 
444
447
  Returns
445
448
  -------
446
- npt.NDArray[np.float64]
449
+ npt.NDArray[np.floating]
447
450
  Array of great circle distances in [:math:`m`] between waypoints
448
451
 
449
452
  Examples
@@ -466,7 +469,7 @@ class Flight(GeoVectorDataset):
466
469
  """
467
470
  return geo.segment_haversine(self["longitude"], self["latitude"])
468
471
 
469
- def segment_length(self) -> npt.NDArray[np.float64]:
472
+ def segment_length(self) -> npt.NDArray[np.floating]:
470
473
  """Compute spherical distance between flight waypoints.
471
474
 
472
475
  Helper function used in :meth:`length` and :meth:`length_met`.
@@ -474,7 +477,7 @@ class Flight(GeoVectorDataset):
474
477
 
475
478
  Returns
476
479
  -------
477
- npt.NDArray[np.float64]
480
+ npt.NDArray[np.floating]
478
481
  Array of distances in [:math:`m`] between waypoints
479
482
 
480
483
  Examples
@@ -496,7 +499,7 @@ class Flight(GeoVectorDataset):
496
499
  """
497
500
  return geo.segment_length(self["longitude"], self["latitude"], self.altitude)
498
501
 
499
- 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]]:
500
503
  """Calculate sine and cosine for the angle between each segment and the longitudinal axis.
501
504
 
502
505
  This is different from the usual navigational angle between two points known as *bearing*.
@@ -516,7 +519,7 @@ class Flight(GeoVectorDataset):
516
519
 
517
520
  Returns
518
521
  -------
519
- npt.NDArray[np.float64], npt.NDArray[np.float64]
522
+ npt.NDArray[np.floating], npt.NDArray[np.floating]
520
523
  Returns ``sin(a), cos(a)``, where ``a`` is the angle between the segment and the
521
524
  longitudinal axis. The final values are of both arrays are ``np.nan``.
522
525
 
@@ -546,7 +549,7 @@ class Flight(GeoVectorDataset):
546
549
  """
547
550
  return geo.segment_angle(self["longitude"], self["latitude"])
548
551
 
549
- def segment_azimuth(self) -> npt.NDArray[np.float64]:
552
+ def segment_azimuth(self) -> npt.NDArray[np.floating]:
550
553
  """Calculate (forward) azimuth at each waypoint.
551
554
 
552
555
  Method calls `pyproj.Geod.inv`, which is slow. See `geo.forward_azimuth`
@@ -558,7 +561,7 @@ class Flight(GeoVectorDataset):
558
561
 
559
562
  Returns
560
563
  -------
561
- npt.NDArray[np.float64]
564
+ npt.NDArray[np.floating]
562
565
  Array of azimuths.
563
566
 
564
567
  See Also
@@ -597,7 +600,7 @@ class Flight(GeoVectorDataset):
597
600
 
598
601
  def segment_groundspeed(
599
602
  self, smooth: bool = False, window_length: int = 7, polyorder: int = 1
600
- ) -> npt.NDArray[np.float64]:
603
+ ) -> npt.NDArray[np.floating]:
601
604
  """Return groundspeed across segments.
602
605
 
603
606
  Calculate by dividing the horizontal segment length by the difference in waypoint times.
@@ -614,7 +617,7 @@ class Flight(GeoVectorDataset):
614
617
 
615
618
  Returns
616
619
  -------
617
- npt.NDArray[np.float64]
620
+ npt.NDArray[np.floating]
618
621
  Groundspeed of the segment, [:math:`m s^{-1}`]
619
622
  """
620
623
  # get horizontal distance (altitude is ignored)
@@ -636,22 +639,22 @@ class Flight(GeoVectorDataset):
636
639
 
637
640
  def segment_true_airspeed(
638
641
  self,
639
- u_wind: npt.NDArray[np.float64] | float = 0.0,
640
- 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,
641
644
  smooth: bool = True,
642
645
  window_length: int = 7,
643
646
  polyorder: int = 1,
644
- ) -> npt.NDArray[np.float64]:
647
+ ) -> npt.NDArray[np.floating]:
645
648
  r"""Calculate the true airspeed [:math:`m/s`] from the ground speed and horizontal winds.
646
649
 
647
650
  The calculated ground speed will first be smoothed with a Savitzky-Golay filter if enabled.
648
651
 
649
652
  Parameters
650
653
  ----------
651
- u_wind : npt.NDArray[np.float64] | float
654
+ u_wind : npt.NDArray[np.floating] | float
652
655
  U wind speed, [:math:`m \ s^{-1}`].
653
656
  Defaults to 0 for all waypoints.
654
- v_wind : npt.NDArray[np.float64] | float
657
+ v_wind : npt.NDArray[np.floating] | float
655
658
  V wind speed, [:math:`m \ s^{-1}`].
656
659
  Defaults to 0 for all waypoints.
657
660
  smooth : bool, optional
@@ -664,7 +667,7 @@ class Flight(GeoVectorDataset):
664
667
 
665
668
  Returns
666
669
  -------
667
- npt.NDArray[np.float64]
670
+ npt.NDArray[np.floating]
668
671
  True wind speed of each segment, [:math:`m \ s^{-1}`]
669
672
  """
670
673
  groundspeed = self.segment_groundspeed(smooth, window_length, polyorder)
@@ -678,39 +681,39 @@ class Flight(GeoVectorDataset):
678
681
  return np.sqrt(tas_x * tas_x + tas_y * tas_y)
679
682
 
680
683
  def segment_mach_number(
681
- self, true_airspeed: npt.NDArray[np.float64], air_temperature: npt.NDArray[np.float64]
682
- ) -> npt.NDArray[np.float64]:
684
+ self, true_airspeed: npt.NDArray[np.floating], air_temperature: npt.NDArray[np.floating]
685
+ ) -> npt.NDArray[np.floating]:
683
686
  r"""Calculate the mach number of each segment.
684
687
 
685
688
  Parameters
686
689
  ----------
687
- true_airspeed : npt.NDArray[np.float64]
690
+ true_airspeed : npt.NDArray[np.floating]
688
691
  True airspeed of the segment, [:math:`m \ s^{-1}`].
689
692
  See :meth:`segment_true_airspeed`.
690
- air_temperature : npt.NDArray[np.float64]
693
+ air_temperature : npt.NDArray[np.floating]
691
694
  Average air temperature of each segment, [:math:`K`]
692
695
 
693
696
  Returns
694
697
  -------
695
- npt.NDArray[np.float64]
698
+ npt.NDArray[np.floating]
696
699
  Mach number of each segment
697
700
  """
698
701
  return units.tas_to_mach_number(true_airspeed, air_temperature)
699
702
 
700
703
  def segment_rocd(
701
704
  self,
702
- air_temperature: None | npt.NDArray[np.float64] = None,
703
- ) -> npt.NDArray[np.float64]:
705
+ air_temperature: None | npt.NDArray[np.floating] = None,
706
+ ) -> npt.NDArray[np.floating]:
704
707
  """Calculate the rate of climb and descent (ROCD).
705
708
 
706
709
  Parameters
707
710
  ----------
708
- air_temperature: None | npt.NDArray[np.float64]
711
+ air_temperature: None | npt.NDArray[np.floating]
709
712
  Air temperature of each flight waypoint, [:math:`K`]
710
713
 
711
714
  Returns
712
715
  -------
713
- npt.NDArray[np.float64]
716
+ npt.NDArray[np.floating]
714
717
  Rate of climb and descent over segment, [:math:`ft min^{-1}`]
715
718
 
716
719
  See Also
@@ -723,7 +726,7 @@ class Flight(GeoVectorDataset):
723
726
  self,
724
727
  threshold_rocd: float = 250.0,
725
728
  min_cruise_altitude_ft: float = 20000.0,
726
- air_temperature: None | npt.NDArray[np.float64] = None,
729
+ air_temperature: None | npt.NDArray[np.floating] = None,
727
730
  ) -> npt.NDArray[np.uint8]:
728
731
  """Identify the phase of flight (climb, cruise, descent) for each segment.
729
732
 
@@ -737,7 +740,7 @@ class Flight(GeoVectorDataset):
737
740
  This is specific for each aircraft type,
738
741
  and can be approximated as 50% of the altitude ceiling.
739
742
  Defaults to 20000 ft.
740
- air_temperature: None | npt.NDArray[np.float64]
743
+ air_temperature: None | npt.NDArray[np.floating]
741
744
  Air temperature of each flight waypoint, [:math:`K`]
742
745
 
743
746
  Returns
@@ -763,7 +766,7 @@ class Flight(GeoVectorDataset):
763
766
  # Filter/Resample
764
767
  # ------------
765
768
 
766
- def filter_by_first(self) -> Flight:
769
+ def filter_by_first(self) -> Self:
767
770
  """Keep first row of group of waypoints with identical coordinates.
768
771
 
769
772
  Chaining this method with `resample_and_fill` often gives a cleaner trajectory
@@ -793,7 +796,7 @@ class Flight(GeoVectorDataset):
793
796
  1 50.0 0.0 0.0 2020-01-01 02:00:00
794
797
  """
795
798
  df = self.dataframe.groupby(["longitude", "latitude"], sort=False).first().reset_index()
796
- return Flight(data=df, attrs=self.attrs)
799
+ return type(self)(data=df, attrs=self.attrs, fuel=self.fuel)
797
800
 
798
801
  def resample_and_fill(
799
802
  self,
@@ -803,8 +806,7 @@ class Flight(GeoVectorDataset):
803
806
  nominal_rocd: float = constants.nominal_rocd,
804
807
  drop: bool = True,
805
808
  keep_original_index: bool = False,
806
- climb_descend_at_end: bool = False,
807
- ) -> Flight:
809
+ ) -> Self:
808
810
  """Resample and fill flight trajectory with geodesics and linear interpolation.
809
811
 
810
812
  Waypoints are resampled according to the frequency ``freq``. Values for :attr:`data`
@@ -842,9 +844,6 @@ class Flight(GeoVectorDataset):
842
844
  Keep the original index of the :class:`Flight` in addition to the new
843
845
  resampled index. Defaults to ``False``.
844
846
  .. versionadded:: 0.45.2
845
- climb_or_descend_at_end : bool
846
- If true, the climb or descent will be placed at the end of each segment
847
- rather than the start. Default is false (climb or descent immediately).
848
847
 
849
848
  Returns
850
849
  -------
@@ -938,16 +937,13 @@ class Flight(GeoVectorDataset):
938
937
 
939
938
  if shift is not None:
940
939
  # We need to translate back to the original chart here
941
- df["longitude"] += shift
942
- df["longitude"] = ((df["longitude"] + 180.0) % 360.0) - 180.0
940
+ df["longitude"] = ((df["longitude"] + shift + 180.0) % 360.0) - 180.0
943
941
 
944
942
  # STEP 6: Interpolate nan values in altitude
945
943
  altitude = df["altitude"].to_numpy()
944
+ time = df.index.to_numpy()
946
945
  if np.any(np.isnan(altitude)):
947
- df_freq = pd.Timedelta(freq).to_numpy()
948
- new_alt = _altitude_interpolation(altitude, nominal_rocd, df_freq, climb_descend_at_end)
949
- _verify_altitude(new_alt, nominal_rocd, df_freq)
950
- df["altitude"] = new_alt
946
+ df["altitude"] = _altitude_interpolation(altitude, time, nominal_rocd)
951
947
 
952
948
  # Remove original index if requested
953
949
  if not keep_original_index:
@@ -961,7 +957,7 @@ class Flight(GeoVectorDataset):
961
957
  msg = f"{msg} Pass 'keep_original_index=True' to keep the original index."
962
958
  warnings.warn(msg)
963
959
 
964
- return Flight(data=df, attrs=self.attrs)
960
+ return type(self)(data=df, attrs=self.attrs, fuel=self.fuel)
965
961
 
966
962
  def clean_and_resample(
967
963
  self,
@@ -974,8 +970,7 @@ class Flight(GeoVectorDataset):
974
970
  force_filter: bool = False,
975
971
  drop: bool = True,
976
972
  keep_original_index: bool = False,
977
- climb_descend_at_end: bool = False,
978
- ) -> Flight:
973
+ ) -> Self:
979
974
  """Resample and (possibly) filter a flight trajectory.
980
975
 
981
976
  Waypoints are resampled according to the frequency ``freq``. If the original
@@ -1016,9 +1011,6 @@ class Flight(GeoVectorDataset):
1016
1011
  Keep the original index of the :class:`Flight` in addition to the new
1017
1012
  resampled index. Defaults to ``False``.
1018
1013
  .. versionadded:: 0.45.2
1019
- climb_or_descend_at_end : bool
1020
- If true, the climb or descent will be placed at the end of each segment
1021
- rather than the start. Default is false (climb or descent immediately).
1022
1014
 
1023
1015
  Returns
1024
1016
  -------
@@ -1037,7 +1029,6 @@ class Flight(GeoVectorDataset):
1037
1029
  nominal_rocd,
1038
1030
  drop,
1039
1031
  keep_original_index,
1040
- climb_descend_at_end,
1041
1032
  )
1042
1033
 
1043
1034
  # If the flight has large gap(s), then call resample and fill, then filter altitude
@@ -1053,7 +1044,6 @@ class Flight(GeoVectorDataset):
1053
1044
  nominal_rocd,
1054
1045
  drop,
1055
1046
  keep_original_index,
1056
- climb_descend_at_end,
1057
1047
  )
1058
1048
  clean_flight = clean_flight.filter_altitude(kernel_size, cruise_threshold)
1059
1049
  else:
@@ -1067,14 +1057,13 @@ class Flight(GeoVectorDataset):
1067
1057
  nominal_rocd,
1068
1058
  drop,
1069
1059
  keep_original_index,
1070
- climb_descend_at_end,
1071
1060
  )
1072
1061
 
1073
1062
  def filter_altitude(
1074
1063
  self,
1075
1064
  kernel_size: int = 17,
1076
1065
  cruise_threshold: float = 120.0,
1077
- ) -> Flight:
1066
+ ) -> Self:
1078
1067
  """
1079
1068
  Filter noisy altitude on a single flight.
1080
1069
 
@@ -1506,7 +1495,7 @@ class Flight(GeoVectorDataset):
1506
1495
  >>> fl["air_temperature"] = fl.intersect_met(met["air_temperature"])
1507
1496
  >>> fl["air_temperature"]
1508
1497
  array([235.94657007, 235.55745645, 235.56709768, ..., 234.59917962,
1509
- 234.60387402, 234.60845312])
1498
+ 234.60387402, 234.60845312], shape=(1081,))
1510
1499
 
1511
1500
  >>> # Length (in meters) of waypoints whose temperature exceeds 236K
1512
1501
  >>> fl.length_met("air_temperature", threshold=236)
@@ -1604,12 +1593,12 @@ class Flight(GeoVectorDataset):
1604
1593
  return ax
1605
1594
 
1606
1595
 
1607
- 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]]:
1608
1597
  """Return list of coordinates for geojson constructions.
1609
1598
 
1610
1599
  Parameters
1611
1600
  ----------
1612
- data : dict[str, npt.NDArray[np.float64]]
1601
+ data : dict[str, npt.NDArray[np.floating]]
1613
1602
  :attr:`data` containing `longitude`, `latitude`, and `altitude` keys
1614
1603
 
1615
1604
  Returns
@@ -1657,15 +1646,15 @@ def _antimeridian_index(longitude: pd.Series) -> list[int]:
1657
1646
 
1658
1647
 
1659
1648
  def _sg_filter(
1660
- vals: npt.NDArray[np.float64], window_length: int = 7, polyorder: int = 1
1661
- ) -> npt.NDArray[np.float64]:
1649
+ vals: npt.NDArray[np.floating], window_length: int = 7, polyorder: int = 1
1650
+ ) -> npt.NDArray[np.floating]:
1662
1651
  """Apply Savitzky-Golay filter to smooth out noise in the time-series data.
1663
1652
 
1664
1653
  Used to smooth true airspeed, fuel flow, and altitude.
1665
1654
 
1666
1655
  Parameters
1667
1656
  ----------
1668
- vals : npt.NDArray[np.float64]
1657
+ vals : npt.NDArray[np.floating]
1669
1658
  Input array
1670
1659
  window_length : int, optional
1671
1660
  Parameter for :func:`scipy.signal.savgol_filter`
@@ -1674,7 +1663,7 @@ def _sg_filter(
1674
1663
 
1675
1664
  Returns
1676
1665
  -------
1677
- npt.NDArray[np.float64]
1666
+ npt.NDArray[np.floating]
1678
1667
  Smoothed values
1679
1668
 
1680
1669
  Raises
@@ -1700,11 +1689,12 @@ def _sg_filter(
1700
1689
 
1701
1690
 
1702
1691
  def _altitude_interpolation(
1703
- altitude: npt.NDArray[np.float64],
1692
+ altitude: npt.NDArray[np.floating],
1693
+ time: npt.NDArray[np.datetime64],
1704
1694
  nominal_rocd: float,
1705
- freq: np.timedelta64,
1706
- climb_or_descend_at_end: bool = False,
1707
- ) -> 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]:
1708
1698
  """Interpolate nan values in ``altitude`` array.
1709
1699
 
1710
1700
  Suppose each group of consecutive nan values is enclosed by ``a0`` and ``a1`` with
@@ -1716,31 +1706,50 @@ def _altitude_interpolation(
1716
1706
 
1717
1707
  Parameters
1718
1708
  ----------
1719
- altitude : npt.NDArray[np.float64]
1709
+ altitude : npt.NDArray[np.floating]
1720
1710
  Array of altitude values containing nan values. This function will raise
1721
1711
  an error if ``altitude`` does not contain nan values. Moreover, this function
1722
- 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.
1723
1715
  nominal_rocd : float
1724
- Nominal rate of climb/descent, in m/s
1725
- freq : np.timedelta64
1726
- Frequency of time index associated to ``altitude``.
1727
- climb_or_descend_at_end : bool
1728
- If true, the climb or descent will be placed at the end of each segment
1729
- 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.
1730
1723
 
1731
1724
  Returns
1732
1725
  -------
1733
- npt.NDArray[np.float64]
1734
- 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.
1735
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
+
1736
1745
  # Determine nan state of altitude
1737
- isna = np.isnan(altitude)
1746
+ isna = np.isnan(alt_ft)
1738
1747
 
1739
- start_na = np.empty(altitude.size, dtype=bool)
1748
+ start_na = np.empty(alt_ft.size, dtype=bool)
1740
1749
  start_na[:-1] = ~isna[:-1] & isna[1:]
1741
1750
  start_na[-1] = False
1742
1751
 
1743
- end_na = np.empty(altitude.size, dtype=bool)
1752
+ end_na = np.empty(alt_ft.size, dtype=bool)
1744
1753
  end_na[0] = False
1745
1754
  end_na[1:] = isna[:-1] & ~isna[1:]
1746
1755
 
@@ -1749,214 +1758,119 @@ def _altitude_interpolation(
1749
1758
  end_na_idxs = np.flatnonzero(end_na)
1750
1759
  na_group_size = end_na_idxs - start_na_idxs
1751
1760
 
1752
- if climb_or_descend_at_end:
1753
- return _altitude_interpolation_climb_descend_end(
1754
- 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
1755
1779
  )
1756
1780
 
1757
- return _altitude_interpolation_climb_descend_middle(
1758
- altitude, start_na_idxs, end_na_idxs, na_group_size, freq, nominal_rocd, isna
1759
- )
1760
-
1761
-
1762
- def _altitude_interpolation_climb_descend_end(
1763
- altitude: npt.NDArray[np.float64],
1764
- na_group_size: npt.NDArray[np.intp],
1765
- nominal_rocd: float,
1766
- freq: np.timedelta64,
1767
- isna: npt.NDArray[np.bool_],
1768
- ) -> npt.NDArray[np.float64]:
1769
- """Interpolate altitude values by placing climbs/descents at end of nan sequences.
1770
-
1771
- The segment will remain at constant elevation until the end of the segment where
1772
- it will climb or descend at a constant rocd based on `nominal_rocd`, reaching the
1773
- target altitude at the end of the segment.
1774
-
1775
- Parameters
1776
- ----------
1777
- altitude : npt.NDArray[np.float64]
1778
- Array of altitude values containing nan values. This function will raise
1779
- an error if ``altitude`` does not contain nan values. Moreover, this function
1780
- assumes the initial and final entries in ``altitude`` are not nan.
1781
- na_group_size : npt.NDArray[np.intp]
1782
- Array of the length of each consecutive sequence of nan values within the
1783
- array provided to input parameter `altitude`.
1784
- nominal_rocd : float
1785
- Nominal rate of climb/descent, in m/s
1786
- freq : np.timedelta64
1787
- Frequency of time index associated to ``altitude``.
1788
- isna : npt.NDArray[np.bool_]
1789
- Array of boolean values indicating whether or not each entry in `altitude`
1790
- is nan-valued.
1791
- -------
1792
- npt.NDArray[np.float64]
1793
- Altitude after nan values have been filled
1794
- """
1795
- cumalt_list = [np.flip(np.arange(1, size, dtype=float)) for size in na_group_size]
1796
- cumalt = np.concatenate(cumalt_list)
1797
- cumalt = cumalt * nominal_rocd * (freq / np.timedelta64(1, "s"))
1798
-
1799
- # Expand cumalt to the full size of altitude
1800
- nominal_fill = np.zeros_like(altitude)
1801
- nominal_fill[isna] = cumalt
1802
-
1803
- # Use pandas to forward and backfill altitude values
1804
- s = pd.Series(altitude)
1805
- s_ff = s.ffill()
1806
- s_bf = s.bfill()
1807
-
1808
- # Construct altitude values if the flight were to climb / descent throughout
1809
- # group of consecutive nan values. The call to np.minimum / np.maximum cuts
1810
- # the climb / descent off at the terminal altitude of the nan group
1811
- fill_climb = np.maximum(s_ff, s_bf - nominal_fill)
1812
- fill_descent = np.minimum(s_ff, s_bf + nominal_fill)
1813
-
1814
- # Explicitly determine if the flight is in a climb or descent state
1815
- sign = np.full_like(altitude, np.nan)
1816
- sign[~isna] = np.sign(np.diff(altitude[~isna], append=np.nan))
1817
- sign = pd.Series(sign).ffill()
1818
-
1819
- # And return the mess
1820
- return np.where(sign == 1.0, fill_climb, fill_descent)
1821
-
1822
-
1823
- def _altitude_interpolation_climb_descend_middle(
1824
- altitude: npt.NDArray[np.float64],
1825
- start_na_idxs: npt.NDArray[np.intp],
1826
- end_na_idxs: npt.NDArray[np.intp],
1827
- na_group_size: npt.NDArray[np.intp],
1828
- freq: np.timedelta64,
1829
- nominal_rocd: float,
1830
- isna: npt.NDArray[np.bool_],
1831
- ) -> npt.NDArray[np.float64]:
1832
- """Interpolate nan altitude values based on step-climb logic.
1833
-
1834
- For short segments, the climb will be placed at the begining of the segment. For
1835
- long climbs (greater than two hours) the climb will be placed in the middle. For
1836
- all descents, the descent will be placed at the end of the segment.
1837
-
1838
- Parameters
1839
- ----------
1840
- altitude : npt.NDArray[np.float64]
1841
- Array of altitude values containing nan values. This function will raise
1842
- an error if ``altitude`` does not contain nan values. Moreover, this function
1843
- assumes the initial and final entries in ``altitude`` are not nan.
1844
- start_na_idxs : npt.NDArray[np.intp]
1845
- Array of indices of the array `altitude` that correspond to the last non-nan-
1846
- valued index before a sequence of consequtive nan values.
1847
- end_na_idxs : npt.NDArray[np.intp]
1848
- Array of indices of the array `altitude` that correspond to the first non-nan-
1849
- valued index after a sequence of consequtive nan values.
1850
- na_group_size : npt.NDArray[np.intp]
1851
- Array of the length of each consecutive sequence of nan values within the
1852
- array provided to input parameter `altitude`.
1853
- nominal_rocd : float
1854
- Nominal rate of climb/descent, in m/s
1855
- freq : np.timedelta64
1856
- Frequency of time index associated to ``altitude``.
1857
- isna : npt.NDArray[np.bool_]
1858
- Array of boolean values indicating whether or not each entry in `altitude`
1859
- is nan-valued.
1860
- -------
1861
- npt.NDArray[np.float64]
1862
- Altitude after nan values have been filled
1863
- """
1864
- s = pd.Series(altitude)
1865
-
1866
- # Check to see if we have gaps greater than two hours
1867
- step_threshold = np.timedelta64(2, "h") / freq
1868
- step_groups = na_group_size > step_threshold
1869
- if np.any(step_groups):
1870
- # If there are gaps greater than two hours, step through one by one
1871
- for i, step_group in enumerate(step_groups):
1872
- # Skip short segments and segments that do not climb
1873
- if not step_group:
1874
- continue
1875
- if s[start_na_idxs[i]] >= s[end_na_idxs[i]]:
1876
- continue
1877
-
1878
- # We have a long climbing segment. Keep first half of segment at the starting
1879
- # altitude, then climb at mid point. Adjust indicies computed before accordingly
1880
- na_group_size[i], is_odd = divmod(na_group_size[i], 2)
1881
- nan_fill_size = na_group_size[i] + is_odd
1882
-
1883
- sl = slice(start_na_idxs[i], start_na_idxs[i] + nan_fill_size + 1)
1884
- isna[sl] = False
1885
- s[sl] = s[start_na_idxs[i]]
1886
- start_na_idxs[i] += nan_fill_size
1887
-
1888
- # Use pandas to forward and backfill altitude values
1889
- s_ff = s.ffill()
1890
- s_bf = s.bfill()
1891
-
1892
- # Form array of cumulative altitude values if the flight were to climb
1893
- # at nominal_rocd over each group of nan
1894
- cumalt_list = []
1895
- for start_na_idx, end_na_idx, size in zip(
1896
- start_na_idxs, end_na_idxs, na_group_size, strict=True
1897
- ):
1898
- if s[start_na_idx] <= s[end_na_idx]:
1899
- cumalt_list.append(np.arange(1, size, dtype=float))
1900
- else:
1901
- cumalt_list.append(np.flip(np.arange(1, size, dtype=float)))
1902
-
1903
- cumalt = np.concatenate(cumalt_list)
1904
- cumalt = cumalt * nominal_rocd * (freq / np.timedelta64(1, "s"))
1905
-
1906
- # Expand cumalt to the full size of altitude
1907
- nominal_fill = np.zeros_like(altitude)
1908
- nominal_fill[isna] = cumalt
1909
-
1910
- # Construct altitude values if the flight were to climb / descent throughout
1911
- # group of consecutive nan values. The call to np.minimum / np.maximum cuts
1912
- # the climb / descent off at the terminal altitude of the nan group
1913
- fill_climb = np.minimum(s_ff + nominal_fill, s_bf)
1914
- fill_descent = np.minimum(s_ff, s_bf + nominal_fill)
1915
-
1916
- # Explicitly determine if the flight is in a climb or descent state
1917
- sign = np.full_like(altitude, np.nan)
1918
- sign[~isna] = np.sign(np.diff(s[~isna], append=np.nan))
1919
- 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
+ )
1920
1815
 
1921
- # And return the mess
1922
- 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
+ )
1923
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
1924
1859
 
1925
- def _verify_altitude(
1926
- altitude: npt.NDArray[np.float64], nominal_rocd: float, freq: np.timedelta64
1927
- ) -> None:
1928
- """Confirm that the time derivative of `altitude` does not exceed twice `nominal_rocd`.
1860
+ continue
1929
1861
 
1930
- Parameters
1931
- ----------
1932
- altitude : npt.NDArray[np.float64]
1933
- Array of filled altitude values containing nan values.
1934
- nominal_rocd : float
1935
- Nominal rate of climb/descent, in m/s
1936
- freq : np.timedelta64
1937
- Frequency of time index associated to `altitude`.
1938
- """
1939
- dalt = np.diff(altitude)
1940
- dt = freq / np.timedelta64(1, "s")
1941
- rocd = np.abs(dalt / dt)
1942
- if np.any(rocd > 2.0 * nominal_rocd):
1943
- warnings.warn(
1944
- "Rate of climb/descent values greater than nominal "
1945
- f"({nominal_rocd} m/s) after altitude interpolation"
1946
- )
1947
- if np.any(np.isnan(altitude)):
1948
- warnings.warn(
1949
- f"Found nan values altitude after ({nominal_rocd} m/s) after altitude interpolation"
1950
- )
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())
1951
1865
 
1952
1866
 
1953
1867
  def filter_altitude(
1954
1868
  time: npt.NDArray[np.datetime64],
1955
- altitude_ft: npt.NDArray[np.float64],
1869
+ altitude_ft: npt.NDArray[np.floating],
1956
1870
  kernel_size: int = 17,
1957
1871
  cruise_threshold: float = 120,
1958
- air_temperature: None | npt.NDArray[np.float64] = None,
1959
- ) -> npt.NDArray[np.float64]:
1872
+ air_temperature: None | npt.NDArray[np.floating] = None,
1873
+ ) -> npt.NDArray[np.floating]:
1960
1874
  """
1961
1875
  Filter noisy altitude on a single flight.
1962
1876
 
@@ -1968,19 +1882,19 @@ def filter_altitude(
1968
1882
  ----------
1969
1883
  time : npt.NDArray[np.datetime64]
1970
1884
  Waypoint time in ``np.datetime64`` format.
1971
- altitude_ft : npt.NDArray[np.float64]
1885
+ altitude_ft : npt.NDArray[np.floating]
1972
1886
  Altitude signal in feet
1973
1887
  kernel_size : int, optional
1974
1888
  Passed directly to :func:`scipy.signal.medfilt`, by default 11.
1975
1889
  Passed also to :func:`scipy.signal.medfilt`
1976
1890
  cruise_theshold : int, optional
1977
1891
  Minimal length of time, in seconds, for a flight to be in cruise to apply median filter
1978
- air_temperature: None | npt.NDArray[np.float64]
1892
+ air_temperature: None | npt.NDArray[np.floating]
1979
1893
  Air temperature of each flight waypoint, [:math:`K`]
1980
1894
 
1981
1895
  Returns
1982
1896
  -------
1983
- npt.NDArray[np.float64]
1897
+ npt.NDArray[np.floating]
1984
1898
  Filtered altitude
1985
1899
 
1986
1900
  Notes
@@ -2070,7 +1984,7 @@ def filter_altitude(
2070
1984
 
2071
1985
  def segment_duration(
2072
1986
  time: npt.NDArray[np.datetime64], dtype: npt.DTypeLike = np.float32
2073
- ) -> npt.NDArray[np.float64]:
1987
+ ) -> npt.NDArray[np.floating]:
2074
1988
  """Calculate the time difference between waypoints.
2075
1989
 
2076
1990
  ``np.nan`` appended so the length of the output is the same as number of waypoints.
@@ -2081,13 +1995,13 @@ def segment_duration(
2081
1995
  Waypoint time in ``np.datetime64`` format.
2082
1996
  dtype : np.dtype
2083
1997
  Numpy dtype for time difference.
2084
- Defaults to ``np.float64``
1998
+ Defaults to ``np.float32``
2085
1999
 
2086
2000
  Returns
2087
2001
  -------
2088
- npt.NDArray[np.float64]
2002
+ npt.NDArray[np.floating]
2089
2003
  Time difference between waypoints, [:math:`s`].
2090
- This returns an array with dtype specified by``dtype``.
2004
+ This returns an array with dtype specified by ``dtype``.
2091
2005
  """
2092
2006
  out = np.empty_like(time, dtype=dtype)
2093
2007
  out[-1] = np.nan
@@ -2096,8 +2010,8 @@ def segment_duration(
2096
2010
 
2097
2011
 
2098
2012
  def segment_phase(
2099
- rocd: npt.NDArray[np.float64],
2100
- altitude_ft: npt.NDArray[np.float64],
2013
+ rocd: npt.NDArray[np.floating],
2014
+ altitude_ft: npt.NDArray[np.floating],
2101
2015
  *,
2102
2016
  threshold_rocd: float = 250.0,
2103
2017
  min_cruise_altitude_ft: float = MIN_CRUISE_ALTITUDE,
@@ -2109,7 +2023,7 @@ def segment_phase(
2109
2023
  rocd: pt.NDArray[np.float64]
2110
2024
  Rate of climb and descent across segment, [:math:`ft min^{-1}`].
2111
2025
  See output from :func:`segment_rocd`.
2112
- altitude_ft: npt.NDArray[np.float64]
2026
+ altitude_ft: npt.NDArray[np.floating]
2113
2027
  Altitude, [:math:`ft`]
2114
2028
  threshold_rocd: float, optional
2115
2029
  ROCD threshold to identify climb and descent, [:math:`ft min^{-1}`].
@@ -2159,31 +2073,31 @@ def segment_phase(
2159
2073
 
2160
2074
 
2161
2075
  def segment_rocd(
2162
- segment_duration: npt.NDArray[np.float64],
2163
- altitude_ft: npt.NDArray[np.float64],
2164
- air_temperature: None | npt.NDArray[np.float64] = None,
2165
- ) -> 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]:
2166
2080
  """Calculate the rate of climb and descent (ROCD).
2167
2081
 
2168
2082
  Parameters
2169
2083
  ----------
2170
- segment_duration: npt.NDArray[np.float64]
2084
+ segment_duration: npt.NDArray[np.floating]
2171
2085
  Time difference between waypoints, [:math:`s`].
2172
- Expected to have numeric `dtype`, not `"timedelta64"`.
2086
+ Expected to have numeric ``dtype``, not ``np.timedelta64``.
2173
2087
  See output from :func:`segment_duration`.
2174
- altitude_ft: npt.NDArray[np.float64]
2088
+ altitude_ft: npt.NDArray[np.floating]
2175
2089
  Altitude of each waypoint, [:math:`ft`]
2176
- air_temperature: None | npt.NDArray[np.float64]
2090
+ air_temperature: npt.NDArray[np.floating] | None
2177
2091
  Air temperature of each flight waypoint, [:math:`K`]
2178
2092
 
2179
2093
  Returns
2180
2094
  -------
2181
- npt.NDArray[np.float64]
2095
+ npt.NDArray[np.floating]
2182
2096
  Rate of climb and descent over segment, [:math:`ft min^{-1}`]
2183
2097
 
2184
2098
  Notes
2185
2099
  -----
2186
- 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.
2187
2101
  This will improve the accuracy of the estimated ROCD with a temperature correction. The
2188
2102
  estimated ROCD with the temperature correction are expected to differ by up to +-5% compared to
2189
2103
  those without the correction. These differences are important when the ROCD estimates are used
@@ -2191,7 +2105,7 @@ def segment_rocd(
2191
2105
 
2192
2106
  See Also
2193
2107
  --------
2194
- :func:`segment_duration`
2108
+ segment_duration
2195
2109
  """
2196
2110
  dt_min = segment_duration / 60.0
2197
2111
 
@@ -2209,7 +2123,7 @@ def segment_rocd(
2209
2123
  T_correction[:-1] = (air_temperature[:-1] + air_temperature[1:]) / (T_isa[:-1] + T_isa[1:])
2210
2124
  T_correction[-1] = np.nan
2211
2125
 
2212
- return T_correction * out
2126
+ return T_correction * out # type: ignore[return-value]
2213
2127
 
2214
2128
 
2215
2129
  def _resample_to_freq(df: pd.DataFrame, freq: str) -> tuple[pd.DataFrame, pd.DatetimeIndex]: