pycontrails 0.40.1__cp310-cp310-macosx_11_0_arm64.whl → 0.42.0__cp310-cp310-macosx_11_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pycontrails might be problematic. Click here for more details.
- pycontrails/_version.py +2 -2
- pycontrails/core/airports.py +228 -0
- pycontrails/core/datalib.py +8 -4
- pycontrails/core/fleet.py +13 -13
- pycontrails/core/flight.py +311 -86
- pycontrails/core/met.py +78 -78
- pycontrails/core/polygon.py +329 -339
- pycontrails/core/rgi_cython.cpython-310-darwin.so +0 -0
- pycontrails/core/vector.py +63 -51
- pycontrails/datalib/__init__.py +1 -1
- pycontrails/datalib/spire/__init__.py +19 -0
- pycontrails/datalib/spire/spire.py +739 -0
- pycontrails/models/cocip/wind_shear.py +2 -2
- pycontrails/models/emissions/emissions.py +1 -1
- pycontrails/models/humidity_scaling.py +1 -1
- pycontrails/models/issr.py +1 -1
- pycontrails/models/pcr.py +1 -1
- pycontrails/models/sac.py +5 -5
- pycontrails/physics/geo.py +3 -2
- pycontrails/physics/jet.py +66 -113
- {pycontrails-0.40.1.dist-info → pycontrails-0.42.0.dist-info}/METADATA +2 -1
- {pycontrails-0.40.1.dist-info → pycontrails-0.42.0.dist-info}/RECORD +26 -23
- {pycontrails-0.40.1.dist-info → pycontrails-0.42.0.dist-info}/LICENSE +0 -0
- {pycontrails-0.40.1.dist-info → pycontrails-0.42.0.dist-info}/NOTICE +0 -0
- {pycontrails-0.40.1.dist-info → pycontrails-0.42.0.dist-info}/WHEEL +0 -0
- {pycontrails-0.40.1.dist-info → pycontrails-0.42.0.dist-info}/top_level.txt +0 -0
pycontrails/core/flight.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import dataclasses
|
|
6
|
+
import enum
|
|
6
7
|
import logging
|
|
7
8
|
import warnings
|
|
8
9
|
from typing import TYPE_CHECKING, Any
|
|
@@ -26,6 +27,31 @@ if TYPE_CHECKING:
|
|
|
26
27
|
import traffic
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
class FlightPhase(enum.IntEnum):
|
|
31
|
+
"""Flight phase enumeration."""
|
|
32
|
+
|
|
33
|
+
CLIMB = enum.auto()
|
|
34
|
+
CRUISE = enum.auto()
|
|
35
|
+
DESCENT = enum.auto()
|
|
36
|
+
LEVEL_FLIGHT = enum.auto()
|
|
37
|
+
NAN = enum.auto()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
#: Max airport elevation, [:math:`ft`]
|
|
41
|
+
#: See `Daocheng_Yading_Airport <https://en.wikipedia.org/wiki/Daocheng_Yading_Airport>`_
|
|
42
|
+
MAX_AIRPORT_ELEVATION = 15_000.0
|
|
43
|
+
|
|
44
|
+
#: Min estimated cruise altitude, [:math:`ft`]
|
|
45
|
+
MIN_CRUISE_ALTITUDE = 20_000.0
|
|
46
|
+
|
|
47
|
+
#: Short haul duration cutoff, [:math:`s`]
|
|
48
|
+
SHORT_HAUL_DURATION = 3600.0
|
|
49
|
+
|
|
50
|
+
#: Set maximum speed compatible with "on_ground" indicator, [:math:`mph`]
|
|
51
|
+
#: Thresholds assessed based on scatter plot (150 knots = 278 km/h)
|
|
52
|
+
MAX_ON_GROUND_SPEED = 150.0
|
|
53
|
+
|
|
54
|
+
|
|
29
55
|
@dataclasses.dataclass
|
|
30
56
|
class Aircraft:
|
|
31
57
|
"""Base class for the physical parameters of the aircraft."""
|
|
@@ -52,51 +78,12 @@ class Aircraft:
|
|
|
52
78
|
max_altitude: float | None = None
|
|
53
79
|
|
|
54
80
|
|
|
55
|
-
@dataclasses.dataclass
|
|
56
|
-
class FlightPhase:
|
|
57
|
-
"""Container for boolean arrays describing phase of the flight.
|
|
58
|
-
|
|
59
|
-
Each array is expected to have the same shape. Arrays should be pairwise disjoint and cover
|
|
60
|
-
each waypoint (for each waypoint, exactly one of the four arrays should be True.)
|
|
61
|
-
|
|
62
|
-
.. todo::
|
|
63
|
-
|
|
64
|
-
Refactor to include this data as property in :class:`Flight` with enum for FlightPhase
|
|
65
|
-
"""
|
|
66
|
-
|
|
67
|
-
cruise: np.ndarray
|
|
68
|
-
climb: np.ndarray
|
|
69
|
-
descent: np.ndarray
|
|
70
|
-
nan: np.ndarray
|
|
71
|
-
|
|
72
|
-
@classmethod
|
|
73
|
-
def all_cruise(cls, size: int) -> FlightPhase:
|
|
74
|
-
"""Generate `FlightPhase` instance in which all waypoints are at cruise.
|
|
75
|
-
|
|
76
|
-
Parameters
|
|
77
|
-
----------
|
|
78
|
-
size : int
|
|
79
|
-
Number of waypoints
|
|
80
|
-
|
|
81
|
-
Returns
|
|
82
|
-
-------
|
|
83
|
-
FlightPhase
|
|
84
|
-
All waypoints are given a cruise state..
|
|
85
|
-
"""
|
|
86
|
-
return cls(
|
|
87
|
-
cruise=np.ones(size).astype(bool),
|
|
88
|
-
climb=np.zeros(size).astype(bool),
|
|
89
|
-
descent=np.zeros(size).astype(bool),
|
|
90
|
-
nan=np.zeros(size).astype(bool),
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
|
|
94
81
|
class Flight(GeoVectorDataset):
|
|
95
82
|
"""A single flight trajectory.
|
|
96
83
|
|
|
97
|
-
Expect latitude-longitude
|
|
84
|
+
Expect latitude-longitude coordinates in WGS 84.
|
|
98
85
|
Expect altitude in [:math:`m`].
|
|
99
|
-
Expect level in [:math:`hPa`].
|
|
86
|
+
Expect pressure level (`level`) in [:math:`hPa`].
|
|
100
87
|
|
|
101
88
|
Use the attribute :attr:`attrs["crs"]` to specify coordinate reference system
|
|
102
89
|
using `PROJ <https://proj.org/>`_ or `EPSG <https://epsg.org/home.html>`_ syntax.
|
|
@@ -110,19 +97,19 @@ class Flight(GeoVectorDataset):
|
|
|
110
97
|
will override ``data`` inputs. Expects ``altitude`` in meters and ``time`` as a
|
|
111
98
|
DatetimeLike (or array that can processed with :func:`pd.to_datetime`).
|
|
112
99
|
Additional waypoint-specific data can be included as additional keys/columns.
|
|
113
|
-
longitude :
|
|
100
|
+
longitude : npt.ArrayLike, optional
|
|
114
101
|
Flight trajectory waypoint longitude.
|
|
115
102
|
Defaults to None.
|
|
116
|
-
latitude :
|
|
103
|
+
latitude : npt.ArrayLike, optional
|
|
117
104
|
Flight trajectory waypoint latitude.
|
|
118
105
|
Defaults to None.
|
|
119
|
-
altitude :
|
|
106
|
+
altitude : npt.ArrayLike, optional
|
|
120
107
|
Flight trajectory waypoint altitude, [:math:`m`].
|
|
121
108
|
Defaults to None.
|
|
122
|
-
level :
|
|
109
|
+
level : npt.ArrayLike, optional
|
|
123
110
|
Flight trajectory waypoint pressure level, [:math:`hPa`].
|
|
124
111
|
Defaults to None.
|
|
125
|
-
time :
|
|
112
|
+
time : npt.ArrayLike, optional
|
|
126
113
|
Flight trajectory waypoint time.
|
|
127
114
|
Defaults to None.
|
|
128
115
|
attrs : dict[str, Any], optional
|
|
@@ -149,6 +136,12 @@ class Flight(GeoVectorDataset):
|
|
|
149
136
|
Raises if ``data`` input does not contain at least ``time``, ``latitude``, ``longitude``,
|
|
150
137
|
(``altitude`` or ``level``).
|
|
151
138
|
|
|
139
|
+
Notes
|
|
140
|
+
-----
|
|
141
|
+
The `Traffic <https://traffic-viz.github.io/index.html>`_ library has many helpful
|
|
142
|
+
flight processing utilities.
|
|
143
|
+
|
|
144
|
+
See :class:`traffic.core.Flight` for more information.
|
|
152
145
|
|
|
153
146
|
Examples
|
|
154
147
|
--------
|
|
@@ -207,11 +200,15 @@ class Flight(GeoVectorDataset):
|
|
|
207
200
|
|
|
208
201
|
def __init__(
|
|
209
202
|
self,
|
|
210
|
-
data: dict[str,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
203
|
+
data: dict[str, npt.ArrayLike]
|
|
204
|
+
| pd.DataFrame
|
|
205
|
+
| VectorDataDict
|
|
206
|
+
| VectorDataset
|
|
207
|
+
| None = None,
|
|
208
|
+
longitude: npt.ArrayLike | None = None,
|
|
209
|
+
latitude: npt.ArrayLike | None = None,
|
|
210
|
+
altitude: npt.ArrayLike | None = None,
|
|
211
|
+
time: npt.ArrayLike | None = None,
|
|
215
212
|
attrs: dict[str, Any] | AttrDict | None = None,
|
|
216
213
|
copy: bool = True,
|
|
217
214
|
aircraft: Aircraft | None = None,
|
|
@@ -420,7 +417,7 @@ class Flight(GeoVectorDataset):
|
|
|
420
417
|
# Segment Properties
|
|
421
418
|
# ------------
|
|
422
419
|
|
|
423
|
-
def segment_duration(self, dtype: np.dtype = np.dtype(np.float32)) -> np.
|
|
420
|
+
def segment_duration(self, dtype: np.dtype = np.dtype(np.float32)) -> npt.NDArray[np.float_]:
|
|
424
421
|
r"""Compute time elapsed between waypoints in seconds.
|
|
425
422
|
|
|
426
423
|
``np.nan`` appended so the length of the output is the same as number of waypoints.
|
|
@@ -433,14 +430,14 @@ class Flight(GeoVectorDataset):
|
|
|
433
430
|
|
|
434
431
|
Returns
|
|
435
432
|
-------
|
|
436
|
-
np.
|
|
433
|
+
npt.NDArray[np.float_]
|
|
437
434
|
Time difference between waypoints, [:math:`s`].
|
|
438
435
|
Returns an array with dtype specified by``dtype``
|
|
439
436
|
"""
|
|
440
437
|
|
|
441
|
-
return
|
|
438
|
+
return segment_duration(self.data["time"], dtype=dtype)
|
|
442
439
|
|
|
443
|
-
def segment_length(self) -> np.
|
|
440
|
+
def segment_length(self) -> npt.NDArray[np.float_]:
|
|
444
441
|
"""Compute spherical distance between flight waypoints.
|
|
445
442
|
|
|
446
443
|
Helper function used in :meth:`length` and :meth:`length_met`.
|
|
@@ -448,7 +445,7 @@ class Flight(GeoVectorDataset):
|
|
|
448
445
|
|
|
449
446
|
Returns
|
|
450
447
|
-------
|
|
451
|
-
np.
|
|
448
|
+
npt.NDArray[np.float_]
|
|
452
449
|
Array of distances in [:math:`m`] between waypoints
|
|
453
450
|
|
|
454
451
|
Raises
|
|
@@ -478,7 +475,7 @@ class Flight(GeoVectorDataset):
|
|
|
478
475
|
|
|
479
476
|
return geo.segment_length(self["longitude"], self["latitude"], self.altitude)
|
|
480
477
|
|
|
481
|
-
def segment_angle(self) -> tuple[np.
|
|
478
|
+
def segment_angle(self) -> tuple[npt.NDArray[np.float_], npt.NDArray[np.float_]]:
|
|
482
479
|
"""Calculate sin/cos for the angle between each segment and the longitudinal axis.
|
|
483
480
|
|
|
484
481
|
This is different from the usual navigational angle between two points known as *bearing*.
|
|
@@ -498,7 +495,7 @@ class Flight(GeoVectorDataset):
|
|
|
498
495
|
|
|
499
496
|
Returns
|
|
500
497
|
-------
|
|
501
|
-
np.
|
|
498
|
+
npt.NDArray[np.float_], npt.NDArray[np.float_]
|
|
502
499
|
sin(a), cos(a), where ``a`` is the angle between the segment and the longitudinal axis
|
|
503
500
|
The final values are of both arrays are ``np.nan``.
|
|
504
501
|
|
|
@@ -528,7 +525,7 @@ class Flight(GeoVectorDataset):
|
|
|
528
525
|
"""
|
|
529
526
|
return geo.segment_angle(self["longitude"], self["latitude"])
|
|
530
527
|
|
|
531
|
-
def segment_azimuth(self) -> np.
|
|
528
|
+
def segment_azimuth(self) -> npt.NDArray[np.float_]:
|
|
532
529
|
"""Calculate (forward) azimuth at each waypoint.
|
|
533
530
|
|
|
534
531
|
Method calls `pyproj.Geod.inv`, which is slow. See `geo.forward_azimuth`
|
|
@@ -540,7 +537,7 @@ class Flight(GeoVectorDataset):
|
|
|
540
537
|
|
|
541
538
|
Returns
|
|
542
539
|
-------
|
|
543
|
-
np.
|
|
540
|
+
npt.NDArray[np.float_]
|
|
544
541
|
Array of azimuths.
|
|
545
542
|
"""
|
|
546
543
|
lon = self["longitude"]
|
|
@@ -564,7 +561,7 @@ class Flight(GeoVectorDataset):
|
|
|
564
561
|
|
|
565
562
|
def segment_groundspeed(
|
|
566
563
|
self, smooth: bool = False, window_length: int = 7, polyorder: int = 1
|
|
567
|
-
) -> np.
|
|
564
|
+
) -> npt.NDArray[np.float_]:
|
|
568
565
|
"""Return groundspeed across segments.
|
|
569
566
|
|
|
570
567
|
Calculate by dividing the horizontal segment length by the difference in waypoint times.
|
|
@@ -581,7 +578,7 @@ class Flight(GeoVectorDataset):
|
|
|
581
578
|
|
|
582
579
|
Returns
|
|
583
580
|
-------
|
|
584
|
-
np.
|
|
581
|
+
npt.NDArray[np.float_]
|
|
585
582
|
Groundspeed of the segment, [:math:`m s^{-1}`]
|
|
586
583
|
"""
|
|
587
584
|
# get horizontal distance - set altitude to 0
|
|
@@ -653,25 +650,76 @@ class Flight(GeoVectorDataset):
|
|
|
653
650
|
return np.sqrt(tas_x * tas_x + tas_y * tas_y)
|
|
654
651
|
|
|
655
652
|
def segment_mach_number(
|
|
656
|
-
self, true_airspeed: np.
|
|
657
|
-
) -> np.
|
|
653
|
+
self, true_airspeed: npt.NDArray[np.float_], air_temperature: npt.NDArray[np.float_]
|
|
654
|
+
) -> npt.NDArray[np.float_]:
|
|
658
655
|
r"""Calculate the mach number of each segment.
|
|
659
656
|
|
|
660
657
|
Parameters
|
|
661
658
|
----------
|
|
662
|
-
true_airspeed : np.
|
|
659
|
+
true_airspeed : npt.NDArray[np.float_]
|
|
663
660
|
True airspeed of the segment, [:math:`m \ s^{-1}`].
|
|
664
661
|
See :meth:`segment_true_airspeed`.
|
|
665
|
-
air_temperature : np.
|
|
662
|
+
air_temperature : npt.NDArray[np.float_]
|
|
666
663
|
Average air temperature of each segment, [:math:`K`]
|
|
667
664
|
|
|
668
665
|
Returns
|
|
669
666
|
-------
|
|
670
|
-
np.
|
|
667
|
+
npt.NDArray[np.float_]
|
|
671
668
|
Mach number of each segment
|
|
672
669
|
"""
|
|
673
670
|
return units.tas_to_mach_number(true_airspeed, air_temperature)
|
|
674
671
|
|
|
672
|
+
def segment_rocd(self) -> npt.NDArray[np.float_]:
|
|
673
|
+
"""Calculate the rate of climb and descent (ROCD).
|
|
674
|
+
|
|
675
|
+
Returns
|
|
676
|
+
-------
|
|
677
|
+
npt.NDArray[np.float_]
|
|
678
|
+
Rate of climb and descent over segment, [:math:`ft min^{-1}`]
|
|
679
|
+
|
|
680
|
+
See Also
|
|
681
|
+
--------
|
|
682
|
+
:func:`segment_rocd`
|
|
683
|
+
"""
|
|
684
|
+
return segment_rocd(self.segment_duration(), self.altitude_ft)
|
|
685
|
+
|
|
686
|
+
def segment_phase(
|
|
687
|
+
self,
|
|
688
|
+
threshold_rocd: float = 250.0,
|
|
689
|
+
min_cruise_altitude_ft: float = 20000.0,
|
|
690
|
+
) -> npt.NDArray[np.uint8]:
|
|
691
|
+
"""Identify the phase of flight (climb, cruise, descent) for each segment.
|
|
692
|
+
|
|
693
|
+
Parameters
|
|
694
|
+
----------
|
|
695
|
+
threshold_rocd : float, optional
|
|
696
|
+
ROCD threshold to identify climb and descent, [:math:`ft min^{-1}`].
|
|
697
|
+
Currently set to 250 ft/min.
|
|
698
|
+
min_cruise_altitude_ft : float, optional
|
|
699
|
+
Minimum altitude for cruise, [:math:`ft`]
|
|
700
|
+
This is specific for each aircraft type,
|
|
701
|
+
and can be approximated as 50% of the altitude ceiling.
|
|
702
|
+
Defaults to 20000 ft.
|
|
703
|
+
|
|
704
|
+
Returns
|
|
705
|
+
-------
|
|
706
|
+
npt.NDArray[np.uint8]
|
|
707
|
+
Array of values enumerating the flight phase.
|
|
708
|
+
See :attr:`flight.FlightPhase` for enumeration.
|
|
709
|
+
|
|
710
|
+
See Also
|
|
711
|
+
--------
|
|
712
|
+
:attr:`FlightPhase`
|
|
713
|
+
:func:`segment_phase`
|
|
714
|
+
:func:`segment_rocd`
|
|
715
|
+
"""
|
|
716
|
+
return segment_phase(
|
|
717
|
+
self.segment_rocd(),
|
|
718
|
+
self.altitude_ft,
|
|
719
|
+
threshold_rocd=threshold_rocd,
|
|
720
|
+
min_cruise_altitude_ft=min_cruise_altitude_ft,
|
|
721
|
+
)
|
|
722
|
+
|
|
675
723
|
# ------------
|
|
676
724
|
# Filter/Resample
|
|
677
725
|
# ------------
|
|
@@ -840,7 +888,7 @@ class Flight(GeoVectorDataset):
|
|
|
840
888
|
# STEP 5: Resample flight to freq
|
|
841
889
|
df = df.resample(freq).first()
|
|
842
890
|
|
|
843
|
-
# STEP 6: Linearly
|
|
891
|
+
# STEP 6: Linearly interpolate small horizontal gaps and account
|
|
844
892
|
# for previous longitude shift.
|
|
845
893
|
keys = ["latitude", "longitude"]
|
|
846
894
|
df.loc[:, keys] = df.loc[:, keys].interpolate(method="linear")
|
|
@@ -1169,12 +1217,12 @@ class Flight(GeoVectorDataset):
|
|
|
1169
1217
|
return ax
|
|
1170
1218
|
|
|
1171
1219
|
|
|
1172
|
-
def _return_linestring(data: dict[str, np.
|
|
1220
|
+
def _return_linestring(data: dict[str, npt.NDArray[np.float_]]) -> list[list[float]]:
|
|
1173
1221
|
"""Return list of coordinates for geojson constructions.
|
|
1174
1222
|
|
|
1175
1223
|
Parameters
|
|
1176
1224
|
----------
|
|
1177
|
-
data : dict[str, np.
|
|
1225
|
+
data : dict[str, npt.NDArray[np.float_]]
|
|
1178
1226
|
:attr:`data` containing `longitude`, `latitude`, and `altitude` keys
|
|
1179
1227
|
|
|
1180
1228
|
Returns
|
|
@@ -1248,16 +1296,16 @@ def _antimeridian_index(longitude: pd.Series, crs: str = "EPSG:4326") -> int:
|
|
|
1248
1296
|
return -1
|
|
1249
1297
|
|
|
1250
1298
|
|
|
1251
|
-
def _sg_filter(
|
|
1299
|
+
def _sg_filter(
|
|
1300
|
+
vals: npt.NDArray[np.float_], window_length: int = 7, polyorder: int = 1
|
|
1301
|
+
) -> npt.NDArray[np.float_]:
|
|
1252
1302
|
"""Apply Savitzky-Golay filter to smooth out noise in the time-series data.
|
|
1253
1303
|
|
|
1254
|
-
Used to smooth true airspeed
|
|
1255
|
-
|
|
1256
|
-
TODO: move to a more centralized location in the codebase if used again
|
|
1304
|
+
Used to smooth true airspeed, fuel flow, and altitude.
|
|
1257
1305
|
|
|
1258
1306
|
Parameters
|
|
1259
1307
|
----------
|
|
1260
|
-
vals : np.
|
|
1308
|
+
vals : npt.NDArray[np.float_]
|
|
1261
1309
|
Input array
|
|
1262
1310
|
window_length : int, optional
|
|
1263
1311
|
Parameter for :func:`scipy.signal.savgol_filter`
|
|
@@ -1266,7 +1314,7 @@ def _sg_filter(vals: np.ndarray, window_length: int = 7, polyorder: int = 1) ->
|
|
|
1266
1314
|
|
|
1267
1315
|
Returns
|
|
1268
1316
|
-------
|
|
1269
|
-
np.
|
|
1317
|
+
npt.NDArray[np.float_]
|
|
1270
1318
|
Smoothed values
|
|
1271
1319
|
|
|
1272
1320
|
Raises
|
|
@@ -1292,8 +1340,8 @@ def _sg_filter(vals: np.ndarray, window_length: int = 7, polyorder: int = 1) ->
|
|
|
1292
1340
|
|
|
1293
1341
|
|
|
1294
1342
|
def _altitude_interpolation(
|
|
1295
|
-
altitude: np.
|
|
1296
|
-
) -> np.
|
|
1343
|
+
altitude: npt.NDArray[np.float_], nominal_rocd: float, freq: np.timedelta64
|
|
1344
|
+
) -> npt.NDArray[np.float_]:
|
|
1297
1345
|
"""Interpolate nan values in `altitude` array.
|
|
1298
1346
|
|
|
1299
1347
|
Suppose each group of consecutive nan values is enclosed by `a0` and `a1` with
|
|
@@ -1304,7 +1352,7 @@ def _altitude_interpolation(
|
|
|
1304
1352
|
|
|
1305
1353
|
Parameters
|
|
1306
1354
|
----------
|
|
1307
|
-
altitude : np.
|
|
1355
|
+
altitude : npt.NDArray[np.float_]
|
|
1308
1356
|
Array of altitude values containing nan values. This function will raise
|
|
1309
1357
|
an error if `altitude` does not contain nan values. Moreover, this function
|
|
1310
1358
|
assumes the initial and final entries in `altitude` are not nan.
|
|
@@ -1315,7 +1363,7 @@ def _altitude_interpolation(
|
|
|
1315
1363
|
|
|
1316
1364
|
Returns
|
|
1317
1365
|
-------
|
|
1318
|
-
np.
|
|
1366
|
+
npt.NDArray[np.float_]
|
|
1319
1367
|
Altitude after nan values have been filled
|
|
1320
1368
|
"""
|
|
1321
1369
|
# Determine nan state of altitude
|
|
@@ -1360,12 +1408,14 @@ def _altitude_interpolation(
|
|
|
1360
1408
|
return np.where(sign == 1, fill_climb, fill_descent)
|
|
1361
1409
|
|
|
1362
1410
|
|
|
1363
|
-
def _verify_altitude(
|
|
1411
|
+
def _verify_altitude(
|
|
1412
|
+
altitude: npt.NDArray[np.float_], nominal_rocd: float, freq: np.timedelta64
|
|
1413
|
+
) -> None:
|
|
1364
1414
|
"""Confirm that the time derivative of `altitude` does not exceed twice `nominal_rocd`.
|
|
1365
1415
|
|
|
1366
1416
|
Parameters
|
|
1367
1417
|
----------
|
|
1368
|
-
altitude : np.
|
|
1418
|
+
altitude : npt.NDArray[np.float_]
|
|
1369
1419
|
Array of filled altitude values containing nan values.
|
|
1370
1420
|
nominal_rocd : float
|
|
1371
1421
|
Nominal rate of climb/descent, in m/s
|
|
@@ -1386,14 +1436,94 @@ def _verify_altitude(altitude: np.ndarray, nominal_rocd: float, freq: np.timedel
|
|
|
1386
1436
|
)
|
|
1387
1437
|
|
|
1388
1438
|
|
|
1389
|
-
def
|
|
1439
|
+
def filter_altitude(
|
|
1440
|
+
altitude: npt.NDArray[np.float_], *, kernel_size: int = 17
|
|
1441
|
+
) -> npt.NDArray[np.float_]:
|
|
1442
|
+
"""
|
|
1443
|
+
Filter noisy altitude on a single flight.
|
|
1444
|
+
|
|
1445
|
+
Currently runs altitude through a median filter using :func:`scipy.signal.medfilt`
|
|
1446
|
+
with ``kernel_size``, then a Savitzky-Golay filter to filter noise.
|
|
1447
|
+
|
|
1448
|
+
.. todo::
|
|
1449
|
+
|
|
1450
|
+
This method assumes that the time interval between altitude points
|
|
1451
|
+
(:func:`segment_duration`) is moderately small (e.g. minutes).
|
|
1452
|
+
This filter may not work as well when waypoints are close (seconds) or
|
|
1453
|
+
farther apart in time (e.g. 30 minutes).
|
|
1454
|
+
|
|
1455
|
+
The optimal altitude filter is a work in a progress
|
|
1456
|
+
and may change in the future.
|
|
1457
|
+
|
|
1458
|
+
Parameters
|
|
1459
|
+
----------
|
|
1460
|
+
altitude : npt.NDArray[np.float_]
|
|
1461
|
+
Altitude signal
|
|
1462
|
+
kernel_size : int, optional
|
|
1463
|
+
Passed directly to :func:`scipy.signal.medfilt`, by default 11.
|
|
1464
|
+
Passed also to :func:`scipy.signal.medfilt`
|
|
1465
|
+
|
|
1466
|
+
Returns
|
|
1467
|
+
-------
|
|
1468
|
+
npt.NDArray[np.float_]
|
|
1469
|
+
Filtered altitude
|
|
1470
|
+
|
|
1471
|
+
Notes
|
|
1472
|
+
-----
|
|
1473
|
+
Algorithm is derived from :meth:`traffic.core.flight.Flight.filter`.
|
|
1474
|
+
|
|
1475
|
+
The `traffic
|
|
1476
|
+
<https://traffic-viz.github.io/api_reference/traffic.core.flight.html#traffic.core.Flight.filter>`_
|
|
1477
|
+
algorithm also computes thresholds on sliding windows
|
|
1478
|
+
and replaces unacceptable values with NaNs.
|
|
1479
|
+
|
|
1480
|
+
Errors may raised if the ``kernel_size`` is too large.
|
|
1481
|
+
|
|
1482
|
+
See Also
|
|
1483
|
+
--------
|
|
1484
|
+
:meth:`traffic.core.flight.Flight.filter`
|
|
1485
|
+
:func:`scipy.signal.medfilt`
|
|
1486
|
+
""" # noqa: E501
|
|
1487
|
+
if not len(altitude):
|
|
1488
|
+
raise ValueError("Altitude must have non-zero length to filter")
|
|
1489
|
+
|
|
1490
|
+
# The kernel_size must be less than or equal to the number of data points available.
|
|
1491
|
+
kernel_size = min(kernel_size, altitude.size)
|
|
1492
|
+
|
|
1493
|
+
# The kernel_size must be odd.
|
|
1494
|
+
if (kernel_size % 2) == 0:
|
|
1495
|
+
kernel_size -= 1
|
|
1496
|
+
|
|
1497
|
+
# Apply a median filter above a certain threshold
|
|
1498
|
+
altitude_filt = scipy.signal.medfilt(altitude, kernel_size=kernel_size)
|
|
1499
|
+
|
|
1500
|
+
# TODO: I think this makes sense because it smooths the climb/descent phases
|
|
1501
|
+
altitude_filt = _sg_filter(altitude_filt, window_length=kernel_size)
|
|
1502
|
+
|
|
1503
|
+
# TODO: The right way to do this is with a low pass filter at
|
|
1504
|
+
# a reasonable rocd threshold ~300-500 ft/min, e.g.
|
|
1505
|
+
# sos = scipy.signal.butter(4, 250, 'low', output='sos')
|
|
1506
|
+
# return scipy.signal.sosfilt(sos, altitude_filt1)
|
|
1507
|
+
#
|
|
1508
|
+
# Remove noise manually
|
|
1509
|
+
# only remove above max airport elevation
|
|
1510
|
+
d_alt_ft = np.diff(altitude_filt, append=np.nan)
|
|
1511
|
+
is_noise = (np.abs(d_alt_ft) <= 25.0) & (altitude_filt > MAX_AIRPORT_ELEVATION)
|
|
1512
|
+
altitude_filt[is_noise] = np.round(altitude_filt[is_noise], -3)
|
|
1513
|
+
|
|
1514
|
+
return altitude_filt
|
|
1515
|
+
|
|
1516
|
+
|
|
1517
|
+
def segment_duration(
|
|
1390
1518
|
time: npt.NDArray[np.datetime64], dtype: np.dtype = np.dtype(np.float32)
|
|
1391
1519
|
) -> npt.NDArray[np.float_]:
|
|
1392
1520
|
"""Calculate the time difference between waypoints.
|
|
1393
1521
|
|
|
1522
|
+
``np.nan`` appended so the length of the output is the same as number of waypoints.
|
|
1523
|
+
|
|
1394
1524
|
Parameters
|
|
1395
1525
|
----------
|
|
1396
|
-
time : np.
|
|
1526
|
+
time : npt.NDArray[np.datetime64]
|
|
1397
1527
|
Waypoint time in ``np.datetime64`` format.
|
|
1398
1528
|
dtype : np.dtype
|
|
1399
1529
|
Numpy dtype for time difference.
|
|
@@ -1401,11 +1531,106 @@ def _dt_waypoints(
|
|
|
1401
1531
|
|
|
1402
1532
|
Returns
|
|
1403
1533
|
-------
|
|
1404
|
-
np.
|
|
1534
|
+
npt.NDArray[np.float_]
|
|
1405
1535
|
Time difference between waypoints, [:math:`s`].
|
|
1406
|
-
This returns an array with dtype specified by``dtype
|
|
1536
|
+
This returns an array with dtype specified by``dtype``.
|
|
1407
1537
|
"""
|
|
1408
1538
|
out = np.empty_like(time, dtype=dtype)
|
|
1409
1539
|
out[-1] = np.nan
|
|
1410
1540
|
out[:-1] = np.diff(time) / np.timedelta64(1, "s")
|
|
1411
1541
|
return out
|
|
1542
|
+
|
|
1543
|
+
|
|
1544
|
+
def segment_phase(
|
|
1545
|
+
rocd: npt.NDArray[np.float_],
|
|
1546
|
+
altitude_ft: npt.NDArray[np.float_],
|
|
1547
|
+
*,
|
|
1548
|
+
threshold_rocd: float = 250.0,
|
|
1549
|
+
min_cruise_altitude_ft: float = MIN_CRUISE_ALTITUDE,
|
|
1550
|
+
) -> npt.NDArray[np.uint8]:
|
|
1551
|
+
"""Identify the phase of flight (climb, cruise, descent) for each segment.
|
|
1552
|
+
|
|
1553
|
+
Parameters
|
|
1554
|
+
----------
|
|
1555
|
+
rocd: pt.NDArray[np.float_]
|
|
1556
|
+
Rate of climb and descent across segment, [:math:`ft min^{-1}`].
|
|
1557
|
+
See output from :func:`segment_rocd`.
|
|
1558
|
+
altitude_ft: npt.NDArray[np.float_]
|
|
1559
|
+
Altitude, [:math:`ft`]
|
|
1560
|
+
threshold_rocd: float, optional
|
|
1561
|
+
ROCD threshold to identify climb and descent, [:math:`ft min^{-1}`].
|
|
1562
|
+
Defaults to 250 ft/min.
|
|
1563
|
+
min_cruise_altitude_ft: float, optional
|
|
1564
|
+
Minimum threshold altitude for cruise, [:math:`ft`]
|
|
1565
|
+
This is specific for each aircraft type,
|
|
1566
|
+
and can be approximated as 50% of the altitude ceiling.
|
|
1567
|
+
Defaults to :attr:`MIN_CRUISE_ALTITUDE`.
|
|
1568
|
+
|
|
1569
|
+
Returns
|
|
1570
|
+
-------
|
|
1571
|
+
npt.NDArray[np.uint8]
|
|
1572
|
+
Array of values enumerating the flight phase.
|
|
1573
|
+
See :attr:`flight.FlightPhase` for enumeration.
|
|
1574
|
+
|
|
1575
|
+
Notes
|
|
1576
|
+
-----
|
|
1577
|
+
Flight data derived from ADS-B and radar sources could contain noise leading
|
|
1578
|
+
to small changes in altitude and ROCD. Hence, an arbitrary ``threshold_rocd``
|
|
1579
|
+
is specified to identify the different phases of flight.
|
|
1580
|
+
|
|
1581
|
+
The flight phase "level-flight" is when an aircraft is holding at lower altitudes.
|
|
1582
|
+
The cruise phase of flight only occurs above a certain threshold altitude.
|
|
1583
|
+
|
|
1584
|
+
See Also
|
|
1585
|
+
--------
|
|
1586
|
+
:attr:`FlightPhase`
|
|
1587
|
+
:func:`segment_rocd`
|
|
1588
|
+
"""
|
|
1589
|
+
nan = np.isnan(rocd)
|
|
1590
|
+
cruise = (
|
|
1591
|
+
(rocd < threshold_rocd) & (rocd > -threshold_rocd) & (altitude_ft > min_cruise_altitude_ft)
|
|
1592
|
+
)
|
|
1593
|
+
climb = ~cruise & (rocd > 0)
|
|
1594
|
+
descent = ~cruise & (rocd < 0)
|
|
1595
|
+
level_flight = ~(nan | cruise | climb | descent)
|
|
1596
|
+
|
|
1597
|
+
phase = np.empty(rocd.shape, dtype=np.uint8)
|
|
1598
|
+
phase[cruise] = FlightPhase.CRUISE
|
|
1599
|
+
phase[climb] = FlightPhase.CLIMB
|
|
1600
|
+
phase[descent] = FlightPhase.DESCENT
|
|
1601
|
+
phase[level_flight] = FlightPhase.LEVEL_FLIGHT
|
|
1602
|
+
phase[nan] = FlightPhase.NAN
|
|
1603
|
+
|
|
1604
|
+
return phase
|
|
1605
|
+
|
|
1606
|
+
|
|
1607
|
+
def segment_rocd(
|
|
1608
|
+
segment_duration: npt.NDArray[np.float_], altitude_ft: npt.NDArray[np.float_]
|
|
1609
|
+
) -> npt.NDArray[np.float_]:
|
|
1610
|
+
"""Calculate the rate of climb and descent (ROCD).
|
|
1611
|
+
|
|
1612
|
+
Parameters
|
|
1613
|
+
----------
|
|
1614
|
+
segment_duration: npt.NDArray[np.float_]
|
|
1615
|
+
Time difference between waypoints, [:math:`s`].
|
|
1616
|
+
Expected to have numeric `dtype`, not `"timedelta64"`.
|
|
1617
|
+
See output from :func:`segment_duration`.
|
|
1618
|
+
altitude_ft: npt.NDArray[np.float_]
|
|
1619
|
+
Altitude of each waypoint, [:math:`ft`]
|
|
1620
|
+
|
|
1621
|
+
Returns
|
|
1622
|
+
-------
|
|
1623
|
+
npt.NDArray[np.float_]
|
|
1624
|
+
Rate of climb and descent over segment, [:math:`ft min^{-1}`]
|
|
1625
|
+
|
|
1626
|
+
See Also
|
|
1627
|
+
--------
|
|
1628
|
+
:func:`segment_duration`
|
|
1629
|
+
"""
|
|
1630
|
+
dt_min = segment_duration / 60.0
|
|
1631
|
+
|
|
1632
|
+
out = np.empty_like(altitude_ft)
|
|
1633
|
+
out[:-1] = np.diff(altitude_ft) / dt_min[:-1]
|
|
1634
|
+
out[-1] = np.nan
|
|
1635
|
+
|
|
1636
|
+
return out
|