GPS#

Great-circle geometry and motion estimated from positions.

Receivers that log only GPGGA sentences record position but neither speed nor course. uataq.instruments.GPS fills both from the positions when they are missing, and marks every filled value with the boolean columns Speed_Estimated / Course_Estimated. Recorded values are never overwritten.

import uataq

gps = uataq.read_data("trx01", instruments="gps", time_range="2017-03")["gps"]
moving = gps[gps.Speed_m_s > 1.0]  # works in the GPGGA-only years too

Validated against a day of recorded 1 Hz speed and course from the TRAX platform: correlation 0.995, median speed difference 0.00 m/s and median course error 0.4 degrees while moving. Position noise becomes speed noise, so widen estimate_window before thresholding a stationary platform.

GPS helpers: great-circle geometry and motion estimated from positions.

When a receiver logs only GPGGA sentences (position, altitude, fix quality) and not GPRMC, there is no recorded speed or course. Both can be recovered from the positions themselves, which is what estimate_speed_course() does; uataq.instruments.GPS calls it to fill whatever the files did not record.

Formulas from https://www.movable-type.co.uk/scripts/latlong.html. They assume a spherical Earth, which is well under GPS noise at these distances.

uataq.gps.EARTH_RADIUS_M = 6371000.0#

Mean Earth radius (m).

uataq.gps.bearing(lat1, lon1, lat2, lon2)[source]#

Return the initial great-circle bearing from the first point to the second.

Parameters:
  • lat1 (array_like) – Start point, in degrees.

  • lon1 (array_like) – Start point, in degrees.

  • lat2 (array_like) – End point, in degrees.

  • lon2 (array_like) – End point, in degrees.

Returns:

Bearing in degrees clockwise from true north, in [0, 360). Inputs are broadcast together. Coincident points give 0.

Return type:

numpy.ndarray

uataq.gps.estimate_speed_course(latitude, longitude, time=None, window=1, max_gap='60s')[source]#

Estimate speed and course from a sequence of positions.

Both are centered differences: the sample at position i is described by the displacement from i - window to i + window, so the estimate is symmetric in time and does not lag the track. Speed is the great-circle distance over the elapsed time; course is the bearing along it.

Parameters:
  • latitude (array_like) – Positions in degrees, ordered in time. A pandas Series with a DatetimeIndex supplies time on its own.

  • longitude (array_like) – Positions in degrees, ordered in time. A pandas Series with a DatetimeIndex supplies time on its own.

  • time (array_like, optional) – Sample times, convertible by pandas.to_datetime(). Defaults to the index of latitude when that is a Series.

  • window (int) – Number of samples on each side of the centered difference. 1 uses the immediate neighbors. A larger window averages over a longer baseline, which suppresses position noise at the cost of smoothing real accelerations and turns.

  • max_gap (str or pandas.Timedelta, optional) – Longest interval between consecutive samples that may fall inside the difference. Where a larger gap is spanned the estimate is NaN, because a straight chord across a gap is not the path travelled. None disables the check.

Returns:

Speed_m_s and Course_deg, indexed by time (named Time_UTC). The first and last window samples are NaN, as are samples spanning a gap or a non-increasing time step.

Return type:

pandas.DataFrame

Notes

Position noise becomes speed noise: a receiver jittering by a few meters looks like it is moving at a few m/s when differenced over 1 s. Use a longer window (or aggregate first) before thresholding a stationary platform, and treat the course as meaningless wherever the speed is at or below the noise level.

uataq.gps.gps_time_from_time_of_day(logger_time, time_of_day)[source]#

Full GPS datetimes from a receiver’s time of day and the logger’s clock.

A receiver that logs only its time of day (HHMMSS[.ss], e.g. horel’s GTIM) needs a date. Each value takes the date of its logger timestamp, moved a day either way when the two straddle midnight, so the logger clock only has to be within 12 h of GPS time.

Parameters:
  • logger_time (pd.DatetimeIndex) – The logger’s timestamps (naive UTC), one per row.

  • time_of_day (array-like) – The receiver’s time of day as an HHMMSS number, aligned with logger_time. NaN, or a value that isn’t a valid time of day, gives NaT.

Returns:

GPS time (naive UTC), indexed like logger_time.

Return type:

pd.Series

uataq.gps.haversine(lat1, lon1, lat2, lon2, R=6371000.0)[source]#

Great-circle distance between two points (haversine formula).

Parameters:
  • lat1 (array_like) – First point, in degrees.

  • lon1 (array_like) – First point, in degrees.

  • lat2 (array_like) – Second point, in degrees.

  • lon2 (array_like) – Second point, in degrees.

  • R (float) – Sphere radius. The result carries its units. Default is EARTH_RADIUS_M, so distances come back in meters.

Returns:

Distance in the units of R. Inputs are broadcast together, so a single point against an array of points works.

Return type:

numpy.ndarray