pycontrails 0.54.3__cp312-cp312-macosx_11_0_arm64.whl → 0.54.5__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 +54 -29
- pycontrails/core/flight.py +218 -301
- pycontrails/core/interpolation.py +63 -60
- pycontrails/core/met.py +193 -125
- pycontrails/core/models.py +27 -13
- pycontrails/core/polygon.py +15 -15
- pycontrails/core/rgi_cython.cpython-312-darwin.so +0 -0
- pycontrails/core/vector.py +119 -96
- 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 +5 -5
- pycontrails/models/apcemm/utils.py +1 -1
- pycontrails/models/cocip/__init__.py +2 -2
- pycontrails/models/cocip/cocip.py +23 -24
- 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 +37 -96
- pycontrails/models/dry_advection.py +60 -19
- pycontrails/models/emissions/__init__.py +2 -2
- pycontrails/models/emissions/black_carbon.py +108 -108
- pycontrails/models/emissions/emissions.py +87 -87
- pycontrails/models/emissions/ffm2.py +35 -35
- pycontrails/models/humidity_scaling/humidity_scaling.py +23 -23
- pycontrails/models/issr.py +2 -2
- 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 +76 -66
- pycontrails/models/ps_model/ps_model.py +16 -16
- 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.5.dist-info}/METADATA +2 -2
- {pycontrails-0.54.3.dist-info → pycontrails-0.54.5.dist-info}/NOTICE +1 -1
- pycontrails-0.54.5.dist-info/RECORD +111 -0
- pycontrails-0.54.3.dist-info/RECORD +0 -111
- {pycontrails-0.54.3.dist-info → pycontrails-0.54.5.dist-info}/LICENSE +0 -0
- {pycontrails-0.54.3.dist-info → pycontrails-0.54.5.dist-info}/WHEEL +0 -0
- {pycontrails-0.54.3.dist-info → pycontrails-0.54.5.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,12 @@ class Flight(GeoVectorDataset):
|
|
|
963
957
|
msg = f"{msg} Pass 'keep_original_index=True' to keep the original index."
|
|
964
958
|
warnings.warn(msg)
|
|
965
959
|
|
|
966
|
-
|
|
960
|
+
# Reorder columns (this is unimportant but makes the output more canonical)
|
|
961
|
+
coord_names = ("longitude", "latitude", "altitude", "time")
|
|
962
|
+
df = df[[*coord_names, *[c for c in df.columns if c not in set(coord_names)]]]
|
|
963
|
+
|
|
964
|
+
data = {k: v.to_numpy() for k, v in df.items()}
|
|
965
|
+
return type(self)._from_fastpath(data, attrs=self.attrs, fuel=self.fuel)
|
|
967
966
|
|
|
968
967
|
def clean_and_resample(
|
|
969
968
|
self,
|
|
@@ -976,8 +975,7 @@ class Flight(GeoVectorDataset):
|
|
|
976
975
|
force_filter: bool = False,
|
|
977
976
|
drop: bool = True,
|
|
978
977
|
keep_original_index: bool = False,
|
|
979
|
-
|
|
980
|
-
) -> Flight:
|
|
978
|
+
) -> Self:
|
|
981
979
|
"""Resample and (possibly) filter a flight trajectory.
|
|
982
980
|
|
|
983
981
|
Waypoints are resampled according to the frequency ``freq``. If the original
|
|
@@ -1018,9 +1016,6 @@ class Flight(GeoVectorDataset):
|
|
|
1018
1016
|
Keep the original index of the :class:`Flight` in addition to the new
|
|
1019
1017
|
resampled index. Defaults to ``False``.
|
|
1020
1018
|
.. versionadded:: 0.45.2
|
|
1021
|
-
climb_or_descend_at_end : bool
|
|
1022
|
-
If true, the climb or descent will be placed at the end of each segment
|
|
1023
|
-
rather than the start. Default is false (climb or descent immediately).
|
|
1024
1019
|
|
|
1025
1020
|
Returns
|
|
1026
1021
|
-------
|
|
@@ -1039,7 +1034,6 @@ class Flight(GeoVectorDataset):
|
|
|
1039
1034
|
nominal_rocd,
|
|
1040
1035
|
drop,
|
|
1041
1036
|
keep_original_index,
|
|
1042
|
-
climb_descend_at_end,
|
|
1043
1037
|
)
|
|
1044
1038
|
|
|
1045
1039
|
# If the flight has large gap(s), then call resample and fill, then filter altitude
|
|
@@ -1055,7 +1049,6 @@ class Flight(GeoVectorDataset):
|
|
|
1055
1049
|
nominal_rocd,
|
|
1056
1050
|
drop,
|
|
1057
1051
|
keep_original_index,
|
|
1058
|
-
climb_descend_at_end,
|
|
1059
1052
|
)
|
|
1060
1053
|
clean_flight = clean_flight.filter_altitude(kernel_size, cruise_threshold)
|
|
1061
1054
|
else:
|
|
@@ -1069,14 +1062,13 @@ class Flight(GeoVectorDataset):
|
|
|
1069
1062
|
nominal_rocd,
|
|
1070
1063
|
drop,
|
|
1071
1064
|
keep_original_index,
|
|
1072
|
-
climb_descend_at_end,
|
|
1073
1065
|
)
|
|
1074
1066
|
|
|
1075
1067
|
def filter_altitude(
|
|
1076
1068
|
self,
|
|
1077
1069
|
kernel_size: int = 17,
|
|
1078
1070
|
cruise_threshold: float = 120.0,
|
|
1079
|
-
) ->
|
|
1071
|
+
) -> Self:
|
|
1080
1072
|
"""
|
|
1081
1073
|
Filter noisy altitude on a single flight.
|
|
1082
1074
|
|
|
@@ -1508,7 +1500,7 @@ class Flight(GeoVectorDataset):
|
|
|
1508
1500
|
>>> fl["air_temperature"] = fl.intersect_met(met["air_temperature"])
|
|
1509
1501
|
>>> fl["air_temperature"]
|
|
1510
1502
|
array([235.94657007, 235.55745645, 235.56709768, ..., 234.59917962,
|
|
1511
|
-
234.60387402, 234.60845312])
|
|
1503
|
+
234.60387402, 234.60845312], shape=(1081,))
|
|
1512
1504
|
|
|
1513
1505
|
>>> # Length (in meters) of waypoints whose temperature exceeds 236K
|
|
1514
1506
|
>>> fl.length_met("air_temperature", threshold=236)
|
|
@@ -1606,12 +1598,12 @@ class Flight(GeoVectorDataset):
|
|
|
1606
1598
|
return ax
|
|
1607
1599
|
|
|
1608
1600
|
|
|
1609
|
-
def _return_linestring(data: dict[str, npt.NDArray[np.
|
|
1601
|
+
def _return_linestring(data: dict[str, npt.NDArray[np.floating]]) -> list[list[float]]:
|
|
1610
1602
|
"""Return list of coordinates for geojson constructions.
|
|
1611
1603
|
|
|
1612
1604
|
Parameters
|
|
1613
1605
|
----------
|
|
1614
|
-
data : dict[str, npt.NDArray[np.
|
|
1606
|
+
data : dict[str, npt.NDArray[np.floating]]
|
|
1615
1607
|
:attr:`data` containing `longitude`, `latitude`, and `altitude` keys
|
|
1616
1608
|
|
|
1617
1609
|
Returns
|
|
@@ -1659,15 +1651,15 @@ def _antimeridian_index(longitude: pd.Series) -> list[int]:
|
|
|
1659
1651
|
|
|
1660
1652
|
|
|
1661
1653
|
def _sg_filter(
|
|
1662
|
-
vals: npt.NDArray[np.
|
|
1663
|
-
) -> npt.NDArray[np.
|
|
1654
|
+
vals: npt.NDArray[np.floating], window_length: int = 7, polyorder: int = 1
|
|
1655
|
+
) -> npt.NDArray[np.floating]:
|
|
1664
1656
|
"""Apply Savitzky-Golay filter to smooth out noise in the time-series data.
|
|
1665
1657
|
|
|
1666
1658
|
Used to smooth true airspeed, fuel flow, and altitude.
|
|
1667
1659
|
|
|
1668
1660
|
Parameters
|
|
1669
1661
|
----------
|
|
1670
|
-
vals : npt.NDArray[np.
|
|
1662
|
+
vals : npt.NDArray[np.floating]
|
|
1671
1663
|
Input array
|
|
1672
1664
|
window_length : int, optional
|
|
1673
1665
|
Parameter for :func:`scipy.signal.savgol_filter`
|
|
@@ -1676,7 +1668,7 @@ def _sg_filter(
|
|
|
1676
1668
|
|
|
1677
1669
|
Returns
|
|
1678
1670
|
-------
|
|
1679
|
-
npt.NDArray[np.
|
|
1671
|
+
npt.NDArray[np.floating]
|
|
1680
1672
|
Smoothed values
|
|
1681
1673
|
|
|
1682
1674
|
Raises
|
|
@@ -1702,11 +1694,12 @@ def _sg_filter(
|
|
|
1702
1694
|
|
|
1703
1695
|
|
|
1704
1696
|
def _altitude_interpolation(
|
|
1705
|
-
altitude: npt.NDArray[np.
|
|
1697
|
+
altitude: npt.NDArray[np.floating],
|
|
1698
|
+
time: npt.NDArray[np.datetime64],
|
|
1706
1699
|
nominal_rocd: float,
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
) -> npt.NDArray[np.
|
|
1700
|
+
minimum_cruise_altitude_ft: float = 20000.0,
|
|
1701
|
+
assumed_cruise_altitude_ft: float = 30000.0,
|
|
1702
|
+
) -> npt.NDArray[np.floating]:
|
|
1710
1703
|
"""Interpolate nan values in ``altitude`` array.
|
|
1711
1704
|
|
|
1712
1705
|
Suppose each group of consecutive nan values is enclosed by ``a0`` and ``a1`` with
|
|
@@ -1718,31 +1711,50 @@ def _altitude_interpolation(
|
|
|
1718
1711
|
|
|
1719
1712
|
Parameters
|
|
1720
1713
|
----------
|
|
1721
|
-
altitude : npt.NDArray[np.
|
|
1714
|
+
altitude : npt.NDArray[np.floating]
|
|
1722
1715
|
Array of altitude values containing nan values. This function will raise
|
|
1723
1716
|
an error if ``altitude`` does not contain nan values. Moreover, this function
|
|
1724
|
-
assumes the initial and final entries in ``altitude`` are not nan
|
|
1717
|
+
assumes the initial and final entries in ``altitude`` are not nan, [:math:`m`]
|
|
1718
|
+
time : npt.NDArray[np.datetime64]
|
|
1719
|
+
Timestamp at each waypoint. Must be monotonically increasing.
|
|
1725
1720
|
nominal_rocd : float
|
|
1726
|
-
Nominal rate of climb/descent,
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1721
|
+
Nominal rate of climb/descent, [:math:`m s^{-1}`]
|
|
1722
|
+
minimum_cruise_altitude_ft : float
|
|
1723
|
+
Minimium cruising altitude for a given aircraft type, [:math:`ft`].
|
|
1724
|
+
By default, this is 20000.0 ft.
|
|
1725
|
+
assumed_cruise_altitude_ft : float
|
|
1726
|
+
Assumed cruising altitude for a given aircraft type, [:math:`ft`].
|
|
1727
|
+
By default, this is 30000.0 ft.
|
|
1732
1728
|
|
|
1733
1729
|
Returns
|
|
1734
1730
|
-------
|
|
1735
|
-
npt.NDArray[np.
|
|
1736
|
-
Altitude after nan values have been filled
|
|
1731
|
+
npt.NDArray[np.floating]
|
|
1732
|
+
Altitude after nan values have been filled, [:math:`m`]
|
|
1733
|
+
|
|
1734
|
+
Notes
|
|
1735
|
+
-----
|
|
1736
|
+
Default values for ``minimum_cruise_altitude_ft`` and ``assumed_cruise_altitude_ft`` should be
|
|
1737
|
+
provided if aircraft-specific parameters are available to improve the output quality.
|
|
1738
|
+
|
|
1739
|
+
We can assume ``minimum_cruise_altitude_ft`` as 0.5 times the aircraft service ceiling, and
|
|
1740
|
+
``assumed_cruise_altitude_ft`` as 0.8 times the aircraft service ceiling.
|
|
1741
|
+
|
|
1742
|
+
Assume that aircraft will generally prefer to climb to a higher altitude as early as possible,
|
|
1743
|
+
and descent to a lower altitude as late as possible, because a higher altitude can reduce
|
|
1744
|
+
drag and fuel consumption.
|
|
1737
1745
|
"""
|
|
1746
|
+
# Work in units of feet
|
|
1747
|
+
alt_ft = units.m_to_ft(altitude)
|
|
1748
|
+
nominal_rocd_ft_min = units.m_to_ft(nominal_rocd) * 60.0
|
|
1749
|
+
|
|
1738
1750
|
# Determine nan state of altitude
|
|
1739
|
-
isna = np.isnan(
|
|
1751
|
+
isna = np.isnan(alt_ft)
|
|
1740
1752
|
|
|
1741
|
-
start_na = np.empty(
|
|
1753
|
+
start_na = np.empty(alt_ft.size, dtype=bool)
|
|
1742
1754
|
start_na[:-1] = ~isna[:-1] & isna[1:]
|
|
1743
1755
|
start_na[-1] = False
|
|
1744
1756
|
|
|
1745
|
-
end_na = np.empty(
|
|
1757
|
+
end_na = np.empty(alt_ft.size, dtype=bool)
|
|
1746
1758
|
end_na[0] = False
|
|
1747
1759
|
end_na[1:] = isna[:-1] & ~isna[1:]
|
|
1748
1760
|
|
|
@@ -1751,214 +1763,119 @@ def _altitude_interpolation(
|
|
|
1751
1763
|
end_na_idxs = np.flatnonzero(end_na)
|
|
1752
1764
|
na_group_size = end_na_idxs - start_na_idxs
|
|
1753
1765
|
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1766
|
+
# NOTE: Only fill altitude gaps that require special attention
|
|
1767
|
+
# At the end of this for loop, those with NaN altitudes will be filled with pd.interpolate
|
|
1768
|
+
for i in range(len(na_group_size)):
|
|
1769
|
+
alt_ft_start = alt_ft[start_na_idxs[i]]
|
|
1770
|
+
alt_ft_end = alt_ft[end_na_idxs[i]]
|
|
1771
|
+
time_start = time[start_na_idxs[i]]
|
|
1772
|
+
time_end = time[end_na_idxs[i]]
|
|
1773
|
+
|
|
1774
|
+
# Calculate parameters to determine how to interpolate altitude
|
|
1775
|
+
# Time to next waypoint
|
|
1776
|
+
dt_next = (time_end - time_start) / np.timedelta64(1, "m")
|
|
1777
|
+
|
|
1778
|
+
# (1): Unrealistic scenario: first and next known waypoints are at very
|
|
1779
|
+
# low altitudes with a large time gap.
|
|
1780
|
+
is_unrealistic = (
|
|
1781
|
+
dt_next > 60.0
|
|
1782
|
+
and alt_ft_start < minimum_cruise_altitude_ft
|
|
1783
|
+
and alt_ft_end < minimum_cruise_altitude_ft
|
|
1757
1784
|
)
|
|
1758
1785
|
|
|
1759
|
-
|
|
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()
|
|
1786
|
+
# If unrealistic, assume flight will climb to cruise altitudes (0.8 * max_altitude_ft),
|
|
1787
|
+
# stay there, and then descent to the next known waypoint
|
|
1788
|
+
if is_unrealistic:
|
|
1789
|
+
# Add altitude at top of climb
|
|
1790
|
+
alt_ft_cruise = assumed_cruise_altitude_ft
|
|
1791
|
+
d_alt_start = alt_ft_cruise - alt_ft_start
|
|
1792
|
+
dt_climb = int(np.ceil(d_alt_start / nominal_rocd_ft_min))
|
|
1793
|
+
t_cruise_start = time_start + np.timedelta64(dt_climb, "m")
|
|
1794
|
+
idx_cruise_start = np.searchsorted(time, t_cruise_start) + 1
|
|
1795
|
+
alt_ft[idx_cruise_start] = alt_ft_cruise
|
|
1796
|
+
|
|
1797
|
+
# Add altitude at top of descent
|
|
1798
|
+
d_alt_end = alt_ft_cruise - alt_ft_end
|
|
1799
|
+
dt_descent = int(np.ceil(d_alt_end / nominal_rocd_ft_min))
|
|
1800
|
+
t_cruise_end = time_end - np.timedelta64(dt_descent, "m")
|
|
1801
|
+
idx_cruise_end = np.searchsorted(time, t_cruise_end) - 1
|
|
1802
|
+
alt_ft[idx_cruise_end] = alt_ft_cruise
|
|
1803
|
+
continue
|
|
1804
|
+
|
|
1805
|
+
# (2): If both altitudes are the same, then skip entire operations below
|
|
1806
|
+
if alt_ft_start == alt_ft_end:
|
|
1807
|
+
continue
|
|
1808
|
+
|
|
1809
|
+
# Rate of climb and descent to next waypoint, in ft/min
|
|
1810
|
+
rocd_next = (alt_ft_end - alt_ft_start) / dt_next
|
|
1811
|
+
|
|
1812
|
+
# (3): If cruise over 2 h with small altitude change, set change to mid-point
|
|
1813
|
+
is_long_segment_small_altitude_change = (
|
|
1814
|
+
dt_next > 120.0
|
|
1815
|
+
and rocd_next < 500.0
|
|
1816
|
+
and rocd_next > -500.0
|
|
1817
|
+
and alt_ft_start > minimum_cruise_altitude_ft
|
|
1818
|
+
and alt_ft_end > minimum_cruise_altitude_ft
|
|
1819
|
+
)
|
|
1922
1820
|
|
|
1923
|
-
|
|
1924
|
-
|
|
1821
|
+
if is_long_segment_small_altitude_change:
|
|
1822
|
+
mid_na_idx = int(0.5 * (start_na_idxs[i] + end_na_idxs[i]))
|
|
1823
|
+
alt_ft[mid_na_idx] = alt_ft_start
|
|
1824
|
+
alt_ft[mid_na_idx + 1] = alt_ft_end
|
|
1825
|
+
continue
|
|
1826
|
+
|
|
1827
|
+
# (4): Climb at the start until target altitude and level off if:
|
|
1828
|
+
#: (i) large time gap (`dt_next` > 20 minutes) and positive `rocd`, or
|
|
1829
|
+
#: (ii) shallow climb (0 < `rocd_next` < 500 ft/min) between current and next waypoint
|
|
1830
|
+
|
|
1831
|
+
#: For (i), we only perform this for large time gaps, because we do not want the aircraft to
|
|
1832
|
+
#: constantly climb, level off, and repeat, while the ADS-B waypoints show that it is
|
|
1833
|
+
#: continuously climbing
|
|
1834
|
+
if (dt_next > 20.0 and rocd_next > 0.0) or (0.0 < rocd_next < 500.0):
|
|
1835
|
+
dt_climb = int(np.ceil((alt_ft_end - alt_ft_start) / nominal_rocd_ft_min * 60))
|
|
1836
|
+
t_climb_complete = time_start + np.timedelta64(dt_climb, "s")
|
|
1837
|
+
idx_climb_complete = np.searchsorted(time, t_climb_complete)
|
|
1838
|
+
|
|
1839
|
+
#: [Safeguard for very small `dt_next`] Ensure climb can be performed within the
|
|
1840
|
+
#: interpolated time step. If False, then aircraft will climb between waypoints instead
|
|
1841
|
+
#: of levelling off.
|
|
1842
|
+
if start_na_idxs[i] < idx_climb_complete < end_na_idxs[i]:
|
|
1843
|
+
alt_ft[idx_climb_complete] = alt_ft_end
|
|
1844
|
+
|
|
1845
|
+
continue
|
|
1846
|
+
|
|
1847
|
+
# (5): Descent towards the end until target altitude and level off if:
|
|
1848
|
+
#: (i) large time gap (`dt_next` > 20 minutes) and negative `rocd`, or
|
|
1849
|
+
#: (ii) shallow descent (-250 < `rocd_next` < 0 ft/min) between current and next waypoint
|
|
1850
|
+
if (dt_next > 20.0 and rocd_next < 0.0) or (-250.0 < rocd_next < 0.0):
|
|
1851
|
+
dt_descent = int(np.ceil((alt_ft_start - alt_ft_end) / nominal_rocd_ft_min * 60))
|
|
1852
|
+
t_descent_start = time_end - np.timedelta64(dt_descent, "s")
|
|
1853
|
+
idx_descent_start = np.where(
|
|
1854
|
+
-250.0 < rocd_next < 0.0,
|
|
1855
|
+
np.searchsorted(time, t_descent_start) - 1,
|
|
1856
|
+
np.searchsorted(time, t_descent_start),
|
|
1857
|
+
)
|
|
1925
1858
|
|
|
1859
|
+
#: [Safeguard for very small `dt_next`] Ensure descent can be performed within the
|
|
1860
|
+
#: interpolated time step. If False, then aircraft will descent between waypoints
|
|
1861
|
+
#: instead of levelling off.
|
|
1862
|
+
if start_na_idxs[i] < idx_descent_start < end_na_idxs[i]:
|
|
1863
|
+
alt_ft[idx_descent_start] = alt_ft_start
|
|
1926
1864
|
|
|
1927
|
-
|
|
1928
|
-
altitude: npt.NDArray[np.float64], nominal_rocd: float, freq: np.timedelta64
|
|
1929
|
-
) -> None:
|
|
1930
|
-
"""Confirm that the time derivative of `altitude` does not exceed twice `nominal_rocd`.
|
|
1865
|
+
continue
|
|
1931
1866
|
|
|
1932
|
-
|
|
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
|
-
)
|
|
1867
|
+
# Linearly interpolate between remaining nan values
|
|
1868
|
+
out_alt_ft = pd.Series(alt_ft, index=time).interpolate(method="index")
|
|
1869
|
+
return units.ft_to_m(out_alt_ft.to_numpy())
|
|
1953
1870
|
|
|
1954
1871
|
|
|
1955
1872
|
def filter_altitude(
|
|
1956
1873
|
time: npt.NDArray[np.datetime64],
|
|
1957
|
-
altitude_ft: npt.NDArray[np.
|
|
1874
|
+
altitude_ft: npt.NDArray[np.floating],
|
|
1958
1875
|
kernel_size: int = 17,
|
|
1959
1876
|
cruise_threshold: float = 120,
|
|
1960
|
-
air_temperature: None | npt.NDArray[np.
|
|
1961
|
-
) -> npt.NDArray[np.
|
|
1877
|
+
air_temperature: None | npt.NDArray[np.floating] = None,
|
|
1878
|
+
) -> npt.NDArray[np.floating]:
|
|
1962
1879
|
"""
|
|
1963
1880
|
Filter noisy altitude on a single flight.
|
|
1964
1881
|
|
|
@@ -1970,19 +1887,19 @@ def filter_altitude(
|
|
|
1970
1887
|
----------
|
|
1971
1888
|
time : npt.NDArray[np.datetime64]
|
|
1972
1889
|
Waypoint time in ``np.datetime64`` format.
|
|
1973
|
-
altitude_ft : npt.NDArray[np.
|
|
1890
|
+
altitude_ft : npt.NDArray[np.floating]
|
|
1974
1891
|
Altitude signal in feet
|
|
1975
1892
|
kernel_size : int, optional
|
|
1976
1893
|
Passed directly to :func:`scipy.signal.medfilt`, by default 11.
|
|
1977
1894
|
Passed also to :func:`scipy.signal.medfilt`
|
|
1978
1895
|
cruise_theshold : int, optional
|
|
1979
1896
|
Minimal length of time, in seconds, for a flight to be in cruise to apply median filter
|
|
1980
|
-
air_temperature: None | npt.NDArray[np.
|
|
1897
|
+
air_temperature: None | npt.NDArray[np.floating]
|
|
1981
1898
|
Air temperature of each flight waypoint, [:math:`K`]
|
|
1982
1899
|
|
|
1983
1900
|
Returns
|
|
1984
1901
|
-------
|
|
1985
|
-
npt.NDArray[np.
|
|
1902
|
+
npt.NDArray[np.floating]
|
|
1986
1903
|
Filtered altitude
|
|
1987
1904
|
|
|
1988
1905
|
Notes
|
|
@@ -2072,7 +1989,7 @@ def filter_altitude(
|
|
|
2072
1989
|
|
|
2073
1990
|
def segment_duration(
|
|
2074
1991
|
time: npt.NDArray[np.datetime64], dtype: npt.DTypeLike = np.float32
|
|
2075
|
-
) -> npt.NDArray[np.
|
|
1992
|
+
) -> npt.NDArray[np.floating]:
|
|
2076
1993
|
"""Calculate the time difference between waypoints.
|
|
2077
1994
|
|
|
2078
1995
|
``np.nan`` appended so the length of the output is the same as number of waypoints.
|
|
@@ -2083,13 +2000,13 @@ def segment_duration(
|
|
|
2083
2000
|
Waypoint time in ``np.datetime64`` format.
|
|
2084
2001
|
dtype : np.dtype
|
|
2085
2002
|
Numpy dtype for time difference.
|
|
2086
|
-
Defaults to ``np.
|
|
2003
|
+
Defaults to ``np.float32``
|
|
2087
2004
|
|
|
2088
2005
|
Returns
|
|
2089
2006
|
-------
|
|
2090
|
-
npt.NDArray[np.
|
|
2007
|
+
npt.NDArray[np.floating]
|
|
2091
2008
|
Time difference between waypoints, [:math:`s`].
|
|
2092
|
-
This returns an array with dtype specified by``dtype``.
|
|
2009
|
+
This returns an array with dtype specified by ``dtype``.
|
|
2093
2010
|
"""
|
|
2094
2011
|
out = np.empty_like(time, dtype=dtype)
|
|
2095
2012
|
out[-1] = np.nan
|
|
@@ -2098,8 +2015,8 @@ def segment_duration(
|
|
|
2098
2015
|
|
|
2099
2016
|
|
|
2100
2017
|
def segment_phase(
|
|
2101
|
-
rocd: npt.NDArray[np.
|
|
2102
|
-
altitude_ft: npt.NDArray[np.
|
|
2018
|
+
rocd: npt.NDArray[np.floating],
|
|
2019
|
+
altitude_ft: npt.NDArray[np.floating],
|
|
2103
2020
|
*,
|
|
2104
2021
|
threshold_rocd: float = 250.0,
|
|
2105
2022
|
min_cruise_altitude_ft: float = MIN_CRUISE_ALTITUDE,
|
|
@@ -2111,7 +2028,7 @@ def segment_phase(
|
|
|
2111
2028
|
rocd: pt.NDArray[np.float64]
|
|
2112
2029
|
Rate of climb and descent across segment, [:math:`ft min^{-1}`].
|
|
2113
2030
|
See output from :func:`segment_rocd`.
|
|
2114
|
-
altitude_ft: npt.NDArray[np.
|
|
2031
|
+
altitude_ft: npt.NDArray[np.floating]
|
|
2115
2032
|
Altitude, [:math:`ft`]
|
|
2116
2033
|
threshold_rocd: float, optional
|
|
2117
2034
|
ROCD threshold to identify climb and descent, [:math:`ft min^{-1}`].
|
|
@@ -2161,31 +2078,31 @@ def segment_phase(
|
|
|
2161
2078
|
|
|
2162
2079
|
|
|
2163
2080
|
def segment_rocd(
|
|
2164
|
-
segment_duration: npt.NDArray[np.
|
|
2165
|
-
altitude_ft: npt.NDArray[np.
|
|
2166
|
-
air_temperature:
|
|
2167
|
-
) -> npt.NDArray[np.
|
|
2081
|
+
segment_duration: npt.NDArray[np.floating],
|
|
2082
|
+
altitude_ft: npt.NDArray[np.floating],
|
|
2083
|
+
air_temperature: npt.NDArray[np.floating] | None = None,
|
|
2084
|
+
) -> npt.NDArray[np.floating]:
|
|
2168
2085
|
"""Calculate the rate of climb and descent (ROCD).
|
|
2169
2086
|
|
|
2170
2087
|
Parameters
|
|
2171
2088
|
----------
|
|
2172
|
-
segment_duration: npt.NDArray[np.
|
|
2089
|
+
segment_duration: npt.NDArray[np.floating]
|
|
2173
2090
|
Time difference between waypoints, [:math:`s`].
|
|
2174
|
-
Expected to have numeric
|
|
2091
|
+
Expected to have numeric ``dtype``, not ``np.timedelta64``.
|
|
2175
2092
|
See output from :func:`segment_duration`.
|
|
2176
|
-
altitude_ft: npt.NDArray[np.
|
|
2093
|
+
altitude_ft: npt.NDArray[np.floating]
|
|
2177
2094
|
Altitude of each waypoint, [:math:`ft`]
|
|
2178
|
-
air_temperature:
|
|
2095
|
+
air_temperature: npt.NDArray[np.floating] | None
|
|
2179
2096
|
Air temperature of each flight waypoint, [:math:`K`]
|
|
2180
2097
|
|
|
2181
2098
|
Returns
|
|
2182
2099
|
-------
|
|
2183
|
-
npt.NDArray[np.
|
|
2100
|
+
npt.NDArray[np.floating]
|
|
2184
2101
|
Rate of climb and descent over segment, [:math:`ft min^{-1}`]
|
|
2185
2102
|
|
|
2186
2103
|
Notes
|
|
2187
2104
|
-----
|
|
2188
|
-
The hydrostatic equation will be used to estimate the ROCD if
|
|
2105
|
+
The hydrostatic equation will be used to estimate the ROCD if ``air_temperature`` is provided.
|
|
2189
2106
|
This will improve the accuracy of the estimated ROCD with a temperature correction. The
|
|
2190
2107
|
estimated ROCD with the temperature correction are expected to differ by up to +-5% compared to
|
|
2191
2108
|
those without the correction. These differences are important when the ROCD estimates are used
|
|
@@ -2193,7 +2110,7 @@ def segment_rocd(
|
|
|
2193
2110
|
|
|
2194
2111
|
See Also
|
|
2195
2112
|
--------
|
|
2196
|
-
|
|
2113
|
+
segment_duration
|
|
2197
2114
|
"""
|
|
2198
2115
|
dt_min = segment_duration / 60.0
|
|
2199
2116
|
|
|
@@ -2211,7 +2128,7 @@ def segment_rocd(
|
|
|
2211
2128
|
T_correction[:-1] = (air_temperature[:-1] + air_temperature[1:]) / (T_isa[:-1] + T_isa[1:])
|
|
2212
2129
|
T_correction[-1] = np.nan
|
|
2213
2130
|
|
|
2214
|
-
return T_correction * out
|
|
2131
|
+
return T_correction * out # type: ignore[return-value]
|
|
2215
2132
|
|
|
2216
2133
|
|
|
2217
2134
|
def _resample_to_freq(df: pd.DataFrame, freq: str) -> tuple[pd.DataFrame, pd.DatetimeIndex]:
|