pycontrails 0.54.3__cp312-cp312-macosx_11_0_arm64.whl → 0.54.4__cp312-cp312-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.
- pycontrails/__init__.py +2 -2
- pycontrails/_version.py +2 -2
- pycontrails/core/__init__.py +1 -1
- pycontrails/core/aircraft_performance.py +58 -58
- pycontrails/core/cache.py +7 -7
- pycontrails/core/fleet.py +25 -21
- pycontrails/core/flight.py +213 -301
- pycontrails/core/interpolation.py +56 -56
- pycontrails/core/met.py +48 -39
- pycontrails/core/models.py +25 -11
- pycontrails/core/polygon.py +15 -15
- pycontrails/core/rgi_cython.cpython-312-darwin.so +0 -0
- pycontrails/core/vector.py +22 -22
- pycontrails/datalib/_met_utils/metsource.py +8 -5
- pycontrails/datalib/ecmwf/__init__.py +14 -14
- pycontrails/datalib/ecmwf/common.py +1 -1
- pycontrails/datalib/ecmwf/era5.py +7 -7
- pycontrails/datalib/ecmwf/hres.py +3 -3
- pycontrails/datalib/ecmwf/ifs.py +1 -1
- pycontrails/datalib/gfs/__init__.py +6 -6
- pycontrails/datalib/gfs/gfs.py +2 -2
- pycontrails/datalib/goes.py +5 -5
- pycontrails/ext/empirical_grid.py +1 -1
- pycontrails/models/apcemm/apcemm.py +3 -3
- pycontrails/models/cocip/__init__.py +2 -2
- pycontrails/models/cocip/cocip.py +15 -15
- pycontrails/models/cocip/cocip_params.py +2 -11
- pycontrails/models/cocip/cocip_uncertainty.py +24 -18
- pycontrails/models/cocip/contrail_properties.py +331 -316
- pycontrails/models/cocip/output_formats.py +53 -53
- pycontrails/models/cocip/radiative_forcing.py +135 -131
- pycontrails/models/cocip/radiative_heating.py +135 -135
- pycontrails/models/cocip/unterstrasser_wake_vortex.py +90 -87
- pycontrails/models/cocip/wake_vortex.py +92 -92
- pycontrails/models/cocip/wind_shear.py +8 -8
- pycontrails/models/cocipgrid/cocip_grid.py +93 -87
- pycontrails/models/dry_advection.py +10 -5
- pycontrails/models/emissions/__init__.py +2 -2
- pycontrails/models/emissions/black_carbon.py +108 -108
- pycontrails/models/emissions/emissions.py +85 -85
- pycontrails/models/emissions/ffm2.py +35 -35
- pycontrails/models/humidity_scaling/humidity_scaling.py +23 -23
- pycontrails/models/ps_model/__init__.py +1 -1
- pycontrails/models/ps_model/ps_aircraft_params.py +8 -4
- pycontrails/models/ps_model/ps_grid.py +74 -64
- pycontrails/models/ps_model/ps_model.py +14 -14
- pycontrails/models/ps_model/ps_operational_limits.py +20 -18
- pycontrails/models/tau_cirrus.py +8 -1
- pycontrails/physics/geo.py +67 -67
- pycontrails/physics/jet.py +79 -79
- pycontrails/physics/units.py +14 -14
- pycontrails/utils/json.py +1 -2
- pycontrails/utils/types.py +12 -7
- {pycontrails-0.54.3.dist-info → pycontrails-0.54.4.dist-info}/METADATA +2 -2
- {pycontrails-0.54.3.dist-info → pycontrails-0.54.4.dist-info}/NOTICE +1 -1
- pycontrails-0.54.4.dist-info/RECORD +111 -0
- pycontrails-0.54.3.dist-info/RECORD +0 -111
- {pycontrails-0.54.3.dist-info → pycontrails-0.54.4.dist-info}/LICENSE +0 -0
- {pycontrails-0.54.3.dist-info → pycontrails-0.54.4.dist-info}/WHEEL +0 -0
- {pycontrails-0.54.3.dist-info → pycontrails-0.54.4.dist-info}/top_level.txt +0 -0
pycontrails/core/flight.py
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
642
|
-
v_wind: npt.NDArray[np.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
684
|
-
) -> npt.NDArray[np.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
705
|
-
) -> npt.NDArray[np.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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) ->
|
|
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
|
|
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
|
-
|
|
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"]
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
) ->
|
|
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.
|
|
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.
|
|
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.
|
|
1663
|
-
) -> npt.NDArray[np.
|
|
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.
|
|
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.
|
|
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.
|
|
1692
|
+
altitude: npt.NDArray[np.floating],
|
|
1693
|
+
time: npt.NDArray[np.datetime64],
|
|
1706
1694
|
nominal_rocd: float,
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
) -> npt.NDArray[np.
|
|
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.
|
|
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,
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
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.
|
|
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(
|
|
1746
|
+
isna = np.isnan(alt_ft)
|
|
1740
1747
|
|
|
1741
|
-
start_na = np.empty(
|
|
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(
|
|
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
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
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
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
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
|
-
|
|
1924
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
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.
|
|
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.
|
|
1961
|
-
) -> npt.NDArray[np.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1998
|
+
Defaults to ``np.float32``
|
|
2087
1999
|
|
|
2088
2000
|
Returns
|
|
2089
2001
|
-------
|
|
2090
|
-
npt.NDArray[np.
|
|
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.
|
|
2102
|
-
altitude_ft: npt.NDArray[np.
|
|
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.
|
|
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.
|
|
2165
|
-
altitude_ft: npt.NDArray[np.
|
|
2166
|
-
air_temperature:
|
|
2167
|
-
) -> npt.NDArray[np.
|
|
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.
|
|
2084
|
+
segment_duration: npt.NDArray[np.floating]
|
|
2173
2085
|
Time difference between waypoints, [:math:`s`].
|
|
2174
|
-
Expected to have numeric
|
|
2086
|
+
Expected to have numeric ``dtype``, not ``np.timedelta64``.
|
|
2175
2087
|
See output from :func:`segment_duration`.
|
|
2176
|
-
altitude_ft: npt.NDArray[np.
|
|
2088
|
+
altitude_ft: npt.NDArray[np.floating]
|
|
2177
2089
|
Altitude of each waypoint, [:math:`ft`]
|
|
2178
|
-
air_temperature:
|
|
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.
|
|
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
|
|
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
|
-
|
|
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]:
|