pycontrails 0.54.2__cp310-cp310-win_amd64.whl → 0.54.4__cp310-cp310-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pycontrails might be problematic. Click here for more details.
- pycontrails/__init__.py +2 -2
- pycontrails/_version.py +2 -2
- pycontrails/core/__init__.py +1 -1
- pycontrails/core/aircraft_performance.py +75 -61
- pycontrails/core/cache.py +7 -7
- pycontrails/core/fleet.py +25 -21
- pycontrails/core/flight.py +215 -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.cp310-win_amd64.pyd +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/ecmwf/variables.py +1 -0
- pycontrails/datalib/gfs/__init__.py +6 -6
- pycontrails/datalib/gfs/gfs.py +2 -2
- pycontrails/datalib/goes.py +5 -5
- pycontrails/datalib/landsat.py +5 -8
- pycontrails/datalib/sentinel.py +7 -11
- pycontrails/ext/bada.py +3 -2
- pycontrails/ext/empirical_grid.py +1 -1
- pycontrails/ext/synthetic_flight.py +3 -2
- pycontrails/models/accf.py +40 -19
- pycontrails/models/apcemm/apcemm.py +5 -4
- pycontrails/models/cocip/__init__.py +2 -2
- pycontrails/models/cocip/cocip.py +16 -17
- 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 +118 -107
- pycontrails/models/dry_advection.py +59 -58
- 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 +3 -2
- pycontrails/models/ps_model/ps_aircraft_params.py +11 -6
- pycontrails/models/ps_model/ps_grid.py +256 -60
- pycontrails/models/ps_model/ps_model.py +18 -21
- pycontrails/models/ps_model/ps_operational_limits.py +58 -69
- pycontrails/models/tau_cirrus.py +8 -1
- pycontrails/physics/geo.py +216 -67
- pycontrails/physics/jet.py +220 -90
- pycontrails/physics/static/iata-cargo-load-factors-20241115.csv +71 -0
- pycontrails/physics/static/iata-passenger-load-factors-20241115.csv +71 -0
- pycontrails/physics/units.py +14 -14
- pycontrails/utils/json.py +1 -2
- pycontrails/utils/types.py +12 -7
- {pycontrails-0.54.2.dist-info → pycontrails-0.54.4.dist-info}/METADATA +10 -10
- {pycontrails-0.54.2.dist-info → pycontrails-0.54.4.dist-info}/NOTICE +1 -1
- pycontrails-0.54.4.dist-info/RECORD +111 -0
- {pycontrails-0.54.2.dist-info → pycontrails-0.54.4.dist-info}/WHEEL +1 -1
- pycontrails-0.54.2.dist-info/RECORD +0 -109
- {pycontrails-0.54.2.dist-info → pycontrails-0.54.4.dist-info}/LICENSE +0 -0
- {pycontrails-0.54.2.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
|
|
@@ -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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
640
|
-
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,
|
|
641
644
|
smooth: bool = True,
|
|
642
645
|
window_length: int = 7,
|
|
643
646
|
polyorder: int = 1,
|
|
644
|
-
) -> npt.NDArray[np.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
682
|
-
) -> npt.NDArray[np.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
703
|
-
) -> npt.NDArray[np.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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) ->
|
|
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
|
|
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
|
-
|
|
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"]
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
) ->
|
|
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.
|
|
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.
|
|
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.
|
|
1661
|
-
) -> npt.NDArray[np.
|
|
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.
|
|
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.
|
|
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.
|
|
1692
|
+
altitude: npt.NDArray[np.floating],
|
|
1693
|
+
time: npt.NDArray[np.datetime64],
|
|
1704
1694
|
nominal_rocd: float,
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
) -> npt.NDArray[np.
|
|
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.
|
|
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,
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
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.
|
|
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(
|
|
1746
|
+
isna = np.isnan(alt_ft)
|
|
1738
1747
|
|
|
1739
|
-
start_na = np.empty(
|
|
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(
|
|
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
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
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
|
-
|
|
1758
|
-
|
|
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
|
-
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
|
-
|
|
1922
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
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.
|
|
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.
|
|
1959
|
-
) -> npt.NDArray[np.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1998
|
+
Defaults to ``np.float32``
|
|
2085
1999
|
|
|
2086
2000
|
Returns
|
|
2087
2001
|
-------
|
|
2088
|
-
npt.NDArray[np.
|
|
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.
|
|
2100
|
-
altitude_ft: npt.NDArray[np.
|
|
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.
|
|
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.
|
|
2163
|
-
altitude_ft: npt.NDArray[np.
|
|
2164
|
-
air_temperature:
|
|
2165
|
-
) -> 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]:
|
|
2166
2080
|
"""Calculate the rate of climb and descent (ROCD).
|
|
2167
2081
|
|
|
2168
2082
|
Parameters
|
|
2169
2083
|
----------
|
|
2170
|
-
segment_duration: npt.NDArray[np.
|
|
2084
|
+
segment_duration: npt.NDArray[np.floating]
|
|
2171
2085
|
Time difference between waypoints, [:math:`s`].
|
|
2172
|
-
Expected to have numeric
|
|
2086
|
+
Expected to have numeric ``dtype``, not ``np.timedelta64``.
|
|
2173
2087
|
See output from :func:`segment_duration`.
|
|
2174
|
-
altitude_ft: npt.NDArray[np.
|
|
2088
|
+
altitude_ft: npt.NDArray[np.floating]
|
|
2175
2089
|
Altitude of each waypoint, [:math:`ft`]
|
|
2176
|
-
air_temperature:
|
|
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.
|
|
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
|
|
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
|
-
|
|
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]:
|