How to simplify or resample a trajectory?๏ƒ

ADS-B trajectories consist of a series of geographical coordinates and other physical features relevant to the dynamics of the aircraft. Such time series may be unnecessarily precise in some cases (e.g. straight lines or static aircraft on ground), or contain gaps because of lack of coverage in terms of data receivers.

The traffic library contains functions to deal with such issues, illustrated here with the following sample trajectory:

from traffic.data.samples import texas_longhorn

texas_longhorn

Flight

  • callsign: N56821 (KIWS to KIWS)
  • aircraft: a7475a ยท ๐Ÿ‡บ๐Ÿ‡ธ N56821 (P28A)
  • start: 2017-09-17 21:38:21+00:00
  • stop: 2017-09-18 00:16:44+00:00
  • duration: 0 days 02:38:23
  • sampling rate: 10 second(s)

Trajectory simplification with Douglas-Peucker algorithm๏ƒ

Trajectory simplification is particularly relevant for the ground track of trajectories. The two most famous methods are the Ramer-Douglas-Peucker algorithm and the Visvalingam-Whyatt algorithm.

Flight.simplify(tolerance, altitude=None, z_factor=3.048)๏ƒ

Simplifies a trajectory with Douglas-Peucker algorithm.

The method uses latitude and longitude, projects the trajectory to a conformal projection and applies the algorithm. If x and y features are already present in the DataFrame (after a call to ~traffic.core.flight.Flight.compute_xy() for instance) then this projection is taken into account.

The tolerance parameter must be defined in meters. :rtype: Flight

  • By default, a 2D version of the algorithm is called, unless you pass a column name for altitude.

  • You may scale the z-axis for more relevance (z_factor). The default value works well in most situations.

The method returns a Flight or a 1D mask if you specify return_mask=True.

See also: How to simplify or resample a trajectory?

texas_longhorn.simplify(1e3) | texas_longhorn.simplify(1e4)

Flight

  • callsign: N56821 (KIWS to KIWS)
  • aircraft: a7475a ยท ๐Ÿ‡บ๐Ÿ‡ธ N56821 (P28A)
  • start: 2017-09-17 21:38:21+00:00
  • stop: 2017-09-18 00:16:44+00:00
  • duration: 0 days 02:38:23
  • sampling rate: 190 second(s)

Flight

  • callsign: N56821 (KIWS to KIWS)
  • aircraft: a7475a ยท ๐Ÿ‡บ๐Ÿ‡ธ N56821 (P28A)
  • start: 2017-09-17 21:38:21+00:00
  • stop: 2017-09-18 00:16:44+00:00
  • duration: 0 days 02:38:23
  • sampling rate: 1056 second(s)

Warning

A basic 2D simplification may result in an undesired oversimplification on another channel, esp. the altitude. An extra parameter altitude is available to help to control the simplification for that other feature. The z_factor is designed to give more or less weight to that feature with respect to the 2D ground track.

import altair as alt

encoding = [
    alt.X(
        "utcyearmonthdatehoursminutesseconds(timestamp):T",
        axis=alt.Axis(format="%H:%M", title=None),
    ),
    alt.Y("altitude", title="altitude (in ft)"),
    alt.Color("name:N", title="Simplification"),
]

base = texas_longhorn.chart().transform_calculate(name="'original'")

chart = (
    alt.vconcat(
        alt.layer(
            base.encode(*encoding),
            texas_longhorn.simplify(1e4)
            .chart()
            .transform_calculate(name="'simplify (2D)'")
            .encode(*encoding),
        ).properties(height=200, width=450),
        alt.layer(
            base.encode(*encoding),
            # specify a parameter to take the altitude into account
            texas_longhorn.simplify(1e4, altitude="altitude", z_factor=30.48)
            .chart()
            .transform_calculate(name="'simplify (3D)'")
            .encode(*encoding),
        ).properties(height=200, width=450),
    )
    .configure_axis(
        titleAngle=0,
        titleY=-15,
        titleX=0,
        titleAnchor="start",
        titleFont="Lato",
        titleFontSize=16,
        labelFontSize=12,
    )
    .configure_legend(
        orient="bottom",
        labelFont="Lato",
        labelFontSize=14,
        titleFont="Lato",
        titleFontSize=14,
    )
)
chart

Trajectory resampling with interpolation๏ƒ

Trajectory resampling is a useful tool in the following situations:

  • reducing the number of samples and size of the resulting object;

  • filling missing data in trajectory (interpolation);

  • fitting many trajectories into vectors of equal length.

Flight.resample(rule='1s', how='interpolate', projection=None)๏ƒ

Resample the trajectory at a given frequency or for a target number of samples.

Parameters:
  • rule (str | int) โ€“

    • If the rule is a string representing Offset aliases for time frequencies is passed, then the data is resampled along the timestamp axis, then interpolated (according to the how parameter).

    • If the rule is an integer, the trajectory is resampled to the given number of evenly distributed points per trajectory.

  • how (None | str | dict[str, Iterable[str]]) โ€“

    (default: "interpolate")

    • When the parameter is a string, the method applies to all columns

    • When the parameter is a dictionary with keys as methods (e.g. "interpolate", "ffill") and names of columns as values. Columns not included in any value are left as is.

  • projection (None | str | pyproj.Proj | โ€˜crs.Projection) โ€“

    (default: None)

    • By default, lat/lon are resampled with a linear interpolation;

    • If a projection is passed, the linear interpolation is applied on the x and y dimensions, then lat/lon are reprojected back;

    • If the projection is a string parameter, e.g. "lcc", a projection is created on the fly, centred on the trajectory. This approach is helpful to fill gaps along a great circle.

Return type:

Flight

texas_longhorn.resample("1 min") | texas_longhorn.resample("3 min")

Flight

  • callsign: N56821 (KIWS to KIWS)
  • aircraft: a7475a ยท ๐Ÿ‡บ๐Ÿ‡ธ N56821 (P28A)
  • start: 2017-09-17 21:38:00+00:00
  • stop: 2017-09-18 00:16:00+00:00
  • duration: 0 days 02:38:00
  • sampling rate: 60 second(s)

Flight

  • callsign: N56821 (KIWS to KIWS)
  • aircraft: a7475a ยท ๐Ÿ‡บ๐Ÿ‡ธ N56821 (P28A)
  • start: 2017-09-17 21:36:00+00:00
  • stop: 2017-09-18 00:15:00+00:00
  • duration: 0 days 02:39:00
  • sampling rate: 180 second(s)

Unlike with simplification, resampling is not designed to respect the topology of a trajectory. However, trends in other features (like altitude) are better preserved.

encoding = [
    alt.X(
        "utcyearmonthdatehoursminutesseconds(timestamp):T",
        axis=alt.Axis(format="%H:%M", title=None),
    ),
    alt.Y("altitude", title="altitude (in ft)"),
    alt.Color("name:N", title="Resampling"),
]

base = texas_longhorn.chart().transform_calculate(name="'original'")

chart = (
    alt.vconcat(
        alt.layer(
            base.encode(*encoding),
            texas_longhorn.resample("1 min")
            .chart()
            .transform_calculate(name="'resample (1 min)'")
            .encode(*encoding),
        ).properties(height=200, width=450),
        alt.layer(
            base.encode(*encoding),
            texas_longhorn.resample("5 min")
            .chart()
            .transform_calculate(name="'resample (5 min)'")
            .encode(*encoding),
        ).properties(height=200, width=450),
    )
    .configure_axis(
        titleAngle=0,
        titleY=-15,
        titleX=0,
        titleAnchor="start",
        titleFont="Lato",
        titleFontSize=16,
        labelFontSize=12,
    )
    .configure_legend(
        orient="bottom",
        labelFont="Lato",
        labelFontSize=14,
        titleFont="Lato",
        titleFontSize=14,
    )
)
chart

Tip

In many use cases, it is necessary to align trajectories in a specific frame (e.g. final approach) and get the same number of samples per trajectory.

In the following example, we resample many final approaches to Paris-Orly airport with 30 samples per trajectory. The x-axis displays the distance to runway threshold; because of different ground speeds, all samples are not perfectly aligned on the x-axis, but they are all equally distributed along the time axis.

from traffic.data.samples import quickstart

landing_lfpo = (
    quickstart.next("aligned_on_ils('LFPO')")
    .cumulative_distance(reverse=True)
    .query("cumdist < 10")
    .resample(30)
    .eval()
)

chart = (
    alt.layer(
        *list(
            flight.chart()
            .mark_line(point=alt.OverlayMarkDef(color="#f58518"))
            .encode(
                alt.X(
                    "cumdist",
                    scale=alt.Scale(reverse=True),
                    title="Cumulative distance to runway threshold (in nm)",
                ),
                alt.Y(
                    "altitude",
                    scale=alt.Scale(domain=(0, 4000)),
                    axis=alt.Axis(
                        title="Altitude (in ft)",
                        titleAngle=0,
                        titleY=-15,
                        titleX=0,
                        titleAnchor="start",
                    ),
                ),
            )
            for flight in landing_lfpo
        )
    )
    .properties(height=250)
    .configure_axis(
        titleFont="Lato",
        titleFontSize=16,
        labelFontSize=12,
    )
)

chart