Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

GIS & Geospatial Data (QGIS, GeoPandas, PostGIS)  Module 7 – GNSS, field data and point clouds

GNSS, NavIC, field data and GPS tracks

How a GNSS receiver fixes its position and where the errors come from, NavIC and India's CORS, field data with its accuracy, and clean GPS tracks.

  • Intermediate
  • 25 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Explain how a receiver solves its position and clock from four or more satellites, and how geometry scales the error
  • Describe NavIC and Survey of India's CORS network, and who may use the CORS data
  • Record field data with the accuracy of every position, in QField or as GPX
  • Clean a GPS track by dropping poor fixes and spikes, finding stops and splitting it into trips
  • Protect the people a track describes before it is shared

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain how a receiver solves its position and clock from four or more satellites, and how geometry scales the error
  • Describe NavIC and Survey of India's CORS network, and who may use the CORS data
  • Record field data with the accuracy of every position, in QField or as GPX
  • Clean a GPS track by dropping poor fixes and spikes, finding stops and splitting it into trips
  • Protect the people a track describes before it is shared

Before you start

On this page

A phone logged a field day of 12.53 km as 20.47 km, because standing still for 30 minutes at a field drifted it 774 m and five fixes jumped hundreds of metres. Every GNSS position carries an error, and a track adds them up.

Here you see how a receiver solves its position from satellite ranges and why the same errors give 6 m or 73 m depending on where the satellites are, what NavIC and India’s CORS network add, how to record field data with the accuracy of each point, and how to clean a track into trips, before sharing it without exposing the people in it.

A receiver solves four unknowns from satellite ranges

GNSS (global navigation satellite system): A system of satellites that broadcast their time and orbit so that a receiver can work out its position and clock: GPS, NavIC, Galileo and BeiDou are examples., global navigation satellite systems such as GPS, NavIC, Galileo and BeiDou, work the same way. Each satellite broadcasts the time by its clock and its orbit; a receiver times each signal’s arrival and turns the travel time into a distance. Its own clock is cheap, though, and an error of 1 ms adds 299,792 m to every distance, so each is a pseudorange: A receiver's measured distance to a satellite, the signal's travel time times the speed of light, which includes the error of the receiver's own clock.. The receiver therefore solves four unknowns, east, north, up and its clock error, and needs four satellites at least. Follow one fix:

Interactive · stage by stage From satellite signals to one position fix Follow one fix through the steps a receiver takes, from the signals it hears to the error its satellites' geometry allows.
From satellite signals to one position fix, step 6 of 6. Showing Satellites broadcast, Receiver times signals, Pseudoranges (each +299,792 m at 1 ms), Solve four unknowns (position to 0.000 m), Error from geometry (6.2 m or 73.2 m at 95 %). In focus: Error from geometry.more: least squaresSatellites broadcasttheir time and their orbitReceiver times signalswith its own clockPseudorangeseach +299,792 m at 1 msSolve four unknownsposition to 0.000 mError from geometry6.2 m or 73.2 m at 95 %

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The signals. Every satellite broadcasts the time by its atomic clock and the orbit it is on, so the receiver knows where each signal came from and when it left.
  2. Timing. The receiver notes when each signal arrives, by its own clock. Multiplying the travel time by the speed of light gives a distance to each satellite.
  3. Pseudoranges. A receiver's clock is not an atomic one. If it runs 1 ms fast, every distance comes out 299,792 m too long, the same amount for every satellite, so each is a pseudorange.
  4. Four unknowns. The position has three unknowns and the clock error a fourth, so four satellites give four equations. From four exact pseudoranges the program finds the position to the millimetre and the clock's 1.000000 ms.
  5. More satellites. With six or more satellites the receiver solves by least squares, which spreads the range errors over all of them instead of fitting four exactly.
  6. Geometry. The same 3 m range errors gave 95 % of fixes within 6.2 m with satellites spread over the sky, HDOP 1.2, and within 73.2 m with all six in one part of it, HDOP 12.6.

The program solves fixes from satellites at chosen positions in the sky, so that their errors and their geometry can be set one at a time:

Four unknowns from satellite ranges, under good and poor geometry Python · gnss/fix.py
"""A receiver's position from pseudoranges: four unknowns, east, north, up and the receiver's clock error, solved from
satellite ranges; then the same range errors under good and poor satellite geometry."""
import math
import random

C = 299_792_458.0  # the speed of light, m/s
RANGE = 22_000_000.0  # a satellite of a 20,000 km orbit is about 20,000 to 26,000 km away; one value keeps it simple


def satellite(azimuth, elevation):
    """A satellite's position east, north and up of the receiver, from its azimuth and elevation in degrees."""
    a, e = math.radians(azimuth), math.radians(elevation)
    return (RANGE * math.cos(e) * math.sin(a), RANGE * math.cos(e) * math.cos(a), RANGE * math.sin(e))


def solve(sats, pseudoranges, steps=8):
    """Least squares for (east, north, up, clock error in metres): start at the origin and refine."""
    x = [0.0, 0.0, 0.0, 0.0]
    for _ in range(steps):
        rows, residuals = [], []
        for s, p in zip(sats, pseudoranges):
            d = math.dist(s, x[:3])
            rows.append([(x[k] - s[k]) / d for k in range(3)] + [1.0])
            residuals.append(p - (d + x[3]))
        n = len(rows[0])
        ata = [[sum(r[i] * r[j] for r in rows) for j in range(n)] for i in range(n)]
        atb = [sum(r[i] * v for r, v in zip(rows, residuals)) for i in range(n)]
        step = gauss(ata, atb)
        x = [a + b for a, b in zip(x, step)]
    return x, rows


def gauss(a, b):
    """Solve a·x = b for a small square system (Gaussian elimination with pivoting)."""
    n = len(b)
    m = [row[:] + [v] for row, v in zip(a, b)]
    for i in range(n):
        p = max(range(i, n), key=lambda r: abs(m[r][i]))
        m[i], m[p] = m[p], m[i]
        for r in range(i + 1, n):
            f = m[r][i] / m[i][i]
            m[r] = [x - f * y for x, y in zip(m[r], m[i])]
    out = [0.0] * n
    for i in reversed(range(n)):
        out[i] = (m[i][n] - sum(m[i][k] * out[k] for k in range(i + 1, n))) / m[i][i]
    return out


def dop(rows):
    """Position and horizontal dilution of precision from the geometry: the square roots of (AᵀA)⁻¹'s diagonal."""
    n = len(rows[0])
    ata = [[sum(r[i] * r[j] for r in rows) for j in range(n)] for i in range(n)]
    inv = [gauss(ata, [1.0 if k == j else 0.0 for k in range(n)]) for j in range(n)]  # columns of the inverse
    return math.sqrt(inv[0][0] + inv[1][1] + inv[2][2]), math.sqrt(inv[0][0] + inv[1][1])


clock = 0.001  # the receiver's clock is 1 ms fast
print(f"a clock error of 1 ms is {C * clock:,.0f} m of range on every satellite")
spread = [satellite(az, el) for az, el in ((0, 75), (90, 30), (200, 40), (300, 25), (45, 15), (250, 60))]
exact = [math.dist(s, (0, 0, 0)) + C * clock for s in spread]
x, _ = solve(spread[:4], exact[:4])
print(f"four exact pseudoranges: position {math.hypot(x[0], x[1]):.3f} m from the truth, clock {x[3] / C * 1000:.6f} ms")

rng = random.Random(4)
for name, sats in (("six satellites spread over the sky", spread),
                   ("six satellites in one part of the sky", [satellite(az, el) for az, el in ((100, 20), (115, 35), (130, 25), (145, 40), (125, 55), (110, 45))])):
    errors = []
    for _ in range(200):  # 200 fixes, each with new range errors of 3 m (standard deviation)
        p = [math.dist(s, (0, 0, 0)) + C * clock + rng.gauss(0, 3.0) for s in sats]
        x, rows = solve(sats, p, steps=4)
        errors.append(math.hypot(x[0], x[1]))
    pdop, hdop = dop(rows)
    errors.sort()
    print(f"{name}: PDOP {pdop:.1f}, HDOP {hdop:.1f}; horizontal error, 95 % of fixes within {errors[189]:.1f} m")

urban = [s for s in spread]  # the spread sky, with one low satellite's signal reflected off a wall
p = [math.dist(s, (0, 0, 0)) + C * clock for s in urban]
p[4] += 25.0  # multipath: the reflected path is 25 m longer
x, _ = solve(urban, p)
print(f"one reflected signal, 25 m too long: the fix moves {math.hypot(x[0], x[1]):.1f} m sideways and {x[2]:+.1f} m up")

Output

a clock error of 1 ms is 299,792 m of range on every satellite
four exact pseudoranges: position 0.000 m from the truth, clock 1.000000 ms
six satellites spread over the sky: PDOP 2.3, HDOP 1.2; horizontal error, 95 % of fixes within 6.2 m
six satellites in one part of the sky: PDOP 15.4, HDOP 12.6; horizontal error, 95 % of fixes within 73.2 m
one reflected signal, 25 m too long: the fix moves 8.6 m sideways and +14.1 m up

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fix.py

From four exact pseudoranges it finds the position to the millimetre and the clock’s 1 ms. With random range errors of 3 m (one standard deviation), six satellites spread over the sky put 95 % of fixes within 6.2 m; the same six crowded into one part of the sky put them within 73.2 m. The factor between range errors and position errors has a name, DOP (dilution of precision): A number from the satellites' geometry that multiplies range errors into position errors: low when the satellites are spread over the sky, high when they crowd into one part of it., and the geometry alone sets it: for horizontal positions it is 1.2 for the spread sky and 12.6 for the crowded one (HDOP), and in three dimensions 2.3 and 15.4 (PDOP).

Key idea

A fix is four unknowns solved from four or more ranges, and the satellites’ geometry decides how far the range errors throw it: the same 3 m became 6.2 m or 73.2 m.

Checkpoint

Check: Four unknowns and the geometry of a fix

  1. Question 1 of 2 Why does a receiver need at least four satellites for a position?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It solves four unknowns, east, north, up and its own clock error, and each satellite gives one range

    A pseudorange is the true range plus the receiver's clock error times the speed of light, 299,792 m for 1 ms. With three position unknowns and one clock unknown, four ranges are the minimum.

  2. Question 2 of 2 The same 3 m range errors gave 95 % of fixes within 6.2 m with six satellites spread over the sky, and within 73.2 m with six in one part of it. What explains the gap?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Geometry: crowded satellites give a high dilution of precision, an HDOP of 12.6 against 1.2, which multiplies the same errors

    Dilution of precision measures how geometry turns range errors into position errors. Satellites spread over the sky, low and high, in every direction, give low values and small errors.

Errors come from the sky, the surroundings and the receiver

GPS.gov names the causes that degrade a user’s accuracy: “signal blockage, atmospheric conditions, and receiver design features/quality”, with signals “reflected off buildings or walls” among them. The satellites themselves are good: the US government commits to a daily global average user range error of 2.0 m or less, with 95 % probability, and puts GPS phones within about a 4.9 m radius under open sky (GPS.gov). The last line of the program shows one wall at work: a reflected signal 25 m too long moved the fix 8.6 m sideways and 14.1 m up.

Better positions come from removing the errors that a nearby receiver shares. A reference station at a known position measures what its ranges should be, and the difference, sent to a moving receiver in real time, as in RTK (real-time kinematic positioning): Positioning in which a receiver applies corrections from a reference station, or a network of them, as it measures, reaching centimetres while the corrections arrive., or applied afterwards, cancels most of the satellite clock, orbit and atmosphere errors the two share. GPS.gov notes that dual-frequency receivers and augmentation systems “can enable real-time positioning within a few centimeters”. QField’s documentation puts a phone’s own GNSS at about 5 m, an external receiver at about 1.5 m and receivers corrected over NTRIP at centimetres (QField). Choose for your own work:

Interactive · answer and compare Which way to fix positions in the field? Answer three questions about your field work. The table rules out the methods that cannot serve it and says why.
What accuracy does the work need?
Must each position be known in the field, while you stand on it?
Is the work done by an Indian Entity, as India's 2021 guidelines define one?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Four ways to fix field positions against the five things that decide between them
Method Accuracy Known in the field Equipment Access in India Trees and buildings
A phone's own GNSS Typical accuracy: about 5 m Poor about 5 m in QField's documentation Good live, with nothing to set up Good the phone you have Good nothing to register Poor one reflected signal moved the lesson fix 8.6 m
An external receiver Typical accuracy: about 1.5 m Mixed about 1.5 m Good live, sent to the phone Mixed a receiver to buy and carry Good nothing to register Mixed a better antenna, the same blocked sky
Network RTK from CORS Typical accuracy: centimetres Good centimetres while corrections arrive Good live, with a data connection Poor an RTK receiver and a correction link Mixed registration; CORS data is for Indian Entities Mixed corrections cannot fix signals that are blocked
Post-processed with CORS Typical accuracy: centimetres Good centimetres once the logs are processed Poor known only after the survey Mixed a receiver that logs raw data Mixed registration; CORS data is for Indian Entities Mixed long sessions help, blocked sky still hurts
How the answers decide
  • “What accuracy does the work need?” Under a metre: plot corners, poles: rules out A phone's own GNSS and adds a caveat to An external receiver. About 5 m from a phone is far too coarse for a plot corner or a pole, and about 1.5 m from an external receiver without corrections is still over a metre.
  • “What accuracy does the work need?” Centimetres: a survey or stake-out: rules out A phone's own GNSS and An external receiver. Centimetres need corrections from reference stations, live or after the survey.
  • “What accuracy does the work need?” Metres: to find a place again: adds a caveat to Network RTK from CORS and Post-processed with CORS. Centimetre methods cost far more than finding a place again needs.
  • “Must each position be known in the field, while you stand on it?” Yes, to navigate or stake out: rules out Post-processed with CORS. Post-processing gives its centimetres only after the logs are processed, not while you stand on the point.
  • “Is the work done by an Indian Entity, as India's 2021 guidelines define one?” No, a foreign entity: rules out Network RTK from CORS and Post-processed with CORS. India's 2021 guidelines make CORS data available with ease of access to Indian Entities only.

In short: a phone to find a place again, an external receiver for metre-level points, and corrections from reference stations, live or afterwards, when the work needs centimetres. No correction brings back a signal that a wall or a canopy blocks; plan points with a clear view of the sky.

Checkpoint

Check: Where position errors come from

  1. Question 1 of 2 In a narrow street, one satellite's signal reaches the phone only by bouncing off a wall, 25 m longer than the direct path. What happens to the fix?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It moves, 8.6 m sideways and 14.1 m up in the lesson's program, because one range is too long

    Multipath, signals reflected off buildings or walls, is a local error that no satellite correction removes. Open sky and a good antenna are the remedies.

  2. Question 2 of 2 Why does a correction from a reference station a few kilometres away improve a rover's position, but not fix a signal blocked by trees?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It cancels errors the two receivers share, satellite, atmosphere and clock, while blocked or reflected signals are local to the rover

    Differential methods remove shared errors. Local errors, blockage and multipath, stay, so plan points with a clear view of the sky even with RTK.

NavIC: India's regional navigation satellite system, also called IRNSS: a constellation designed as seven satellites serving India and the region up to 1,500 km beyond it., India’s regional system, is designed for India and the region round it (ISRO):

NavIC As ISRO describes it
Satellites 7: three geostationary and four in inclined geosynchronous orbits
Geostationary at 32.5° E, 83° E and 129.5° E
Inclined orbits two pairs crossing the equator at 55° E and 111.75° E, inclined 29°
Coverage India and up to 1,500 km beyond its boundary
Services Standard Position Service for civilians; Restricted Service
Bands L5 (1176.45 MHz) and S (2498.028 MHz); L1 (1575.42 MHz) on satellites from 2023
Design accuracy better than 20 m in the primary service area

Survey of India's CORS, and who may use its data

GAGAN augments GPS for aviation over Indian airspace. Survey of India runs a network of CORS (Continuously Operating Reference Stations): GNSS receivers fixed at precisely known positions that record without a break, so that their data can correct other receivers, in real time or afterwards. with procedures for network RTK surveys, DGNSS surveys, online post-processing and data download, after registration (Survey of India). India’s 2021 guidelines make CORS data available “without any restrictions” but “with the ease of access to Indian Entities only” (DST guidelines).

The 20 m figure is a design target for NavIC on its own. A phone that receives NavIC as well as GPS sees more satellites in more directions, which, as the first section showed, lowers the dilution of precision. For work that must be repeatable, such as the corners of a plot, the CORS network ties every visit to the same reference stations.

Checkpoint

Check: NavIC and India's CORS

  1. Question 1 of 2 How is NavIC's constellation designed?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 7 satellites, 3 geostationary and 4 in inclined geosynchronous orbits, covering India and 1,500 km beyond

    A regional system can cover its area with fewer satellites by keeping them over it: three fixed over the equator and four whose orbits swing north and south of it.

  2. Question 2 of 2 A foreign survey company wants Survey of India's CORS data for a project in India. What do India's 2021 guidelines say?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: CORS data is made available with ease of access to Indian Entities only

    The guidelines make ground stations and augmentation services, CORS among them, available to Indian Entities only. A foreign company would work through an Indian Entity or use other corrections.

Field data keeps the accuracy of every point

A position without its accuracy cannot be judged later. QField, the field app for QGIS projects, can block digitising while its accuracy indicator is red (“Enable accuracy requirement”), average several fixes into one vertex, subtract the antenna pole’s height, and turn ellipsoidal heights into orthometric ones with a vertical grid file (QField). Its expression variables can fill fields automatically as default values (QField variables):

Field to keep with each point QField variable What it records
Horizontal accuracy @position_horizontal_accuracy metres, as the receiver reports it
Vertical accuracy @position_vertical_accuracy metres, as the receiver reports it
Satellites used @position_number_of_used_satellites the count behind the fix
Quality @position_quality_description such as “Fixed RTK”
Averaged fixes @position_averaged_count 0 when not averaged
Time @position_timestamp the fix’s time in UTC

Tracks travel as GPX: An XML format for GPS data: waypoints, routes and tracks, a track made of segments of points with latitude and longitude in WGS 84 and an optional time., an XML format whose root gpx element must carry version="1.1" and a creator; a trk holds segments, trkseg, of points, trkpt, with lat and lon in decimal degrees of WGS 84 and an optional time (GPX 1.1). GPX has hdop and sat but no field for an accuracy in metres, so keep accuracies in the layer the points live in, such as a GeoPackage. The program writes the cleaned trips of this lesson’s field day as GPX and reads the file back:

The cleaned trips as GPX 1.1, written and read back Python · tracks/gpx.py
"""The cleaned trips written as GPX 1.1, one track segment per trip, then read back and checked."""
import xml.etree.ElementTree as ET

from day import clock, logged
from steps import drop_poor, drop_spikes, find_stops, split_trips
from utm import from_utm

kept, _ = drop_spikes(drop_poor(logged()))
trips, _ = split_trips(kept, find_stops(kept))

NS = "http://www.topografix.com/GPX/1/1"
ET.register_namespace("", NS)
gpx = ET.Element(f"{{{NS}}}gpx", version="1.1", creator="MySmartCoPilot lesson")
trk = ET.SubElement(gpx, f"{{{NS}}}trk")
ET.SubElement(trk, f"{{{NS}}}name").text = "Field day, cleaned"
for trip in trips:
    seg = ET.SubElement(trk, f"{{{NS}}}trkseg")  # GPX: a new segment for each continuous span of the track
    for t, x, y, _ in trip:
        lon, lat = from_utm(x, y)
        pt = ET.SubElement(seg, f"{{{NS}}}trkpt", lat=f"{lat:.7f}", lon=f"{lon:.7f}")
        ET.SubElement(pt, f"{{{NS}}}time").text = f"2025-11-14T{clock(t)}Z"
ET.indent(gpx, "  ")
ET.ElementTree(gpx).write("field_day.gpx", encoding="utf-8", xml_declaration=True)

root = ET.parse("field_day.gpx").getroot()
q = lambda name: f"{{{NS}}}{name}"
segs = root.findall(f"{q('trk')}/{q('trkseg')}")
points = [p for s in segs for p in s.findall(q("trkpt"))]
lats, lons = [float(p.get("lat")) for p in points], [float(p.get("lon")) for p in points]
times = [[p.find(q("time")).text for p in s.findall(q("trkpt"))] for s in segs]
print("field_day.gpx, read back:")
print(f"  version {root.get('version')}, creator '{root.get('creator')}'")
print(f"  {len(segs)} segments, {len(points)} points; latitude {min(lats):.4f} to {max(lats):.4f}, longitude {min(lons):.4f} to {max(lons):.4f}")
print(f"  times in order within every segment: {all(t == sorted(t) for t in times)}")
with open("field_day.gpx", encoding="utf-8") as f:
    print("  " + "\n  ".join(f.read().splitlines()[:7]))

Output

field_day.gpx, read back:
  version 1.1, creator 'MySmartCoPilot lesson'
  4 segments, 332 points; latitude 18.4871 to 18.5686, longitude 73.8017 to 73.8154
  times in order within every segment: True
  <?xml version='1.0' encoding='utf-8'?>
  <gpx xmlns="http://www.topografix.com/GPX/1/1" version="1.1" creator="MySmartCoPilot lesson">
    <trk>
      <name>Field day, cleaned</name>
      <trkseg>
        <trkpt lat="18.4871059" lon="73.8017422">
          <time>2025-11-14T08:00:00Z</time>

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 gpx.py

Each trip is a segment of its own, because the GPX documentation asks for “a new Track Segment for each continuous span of track data”, and every segment’s times come back in order.

Checkpoint

Check: Field data with its accuracy, and GPX

  1. Question 1 of 2 Where should a field team keep each point's horizontal accuracy in metres?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: In a field of the point's layer, filled by @position_horizontal_accuracy as a default value

    QField fills fields from expression variables when a point is captured. Keep the accuracy, the time, the satellites used and the averaging count with every point.

  2. Question 2 of 2 A track has a gap of four minutes in an underpass. How should a GPX file hold it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: As two track segments, trkseg, one for each continuous span

    Segments keep a break in the data visible, and the lesson's cleaned day is four segments, one per trip.

Cleaning a track: poor fixes, spikes, stops and trips

A logged track mixes movement with three kinds of noise: fixes the receiver itself marks as poor, spikes that jump hundreds of metres for one fix, and the drift of a receiver standing still, which adds distance where none was covered. Speed is the test for a spike: a fix that would need 54 m/s to reach is not where anyone went. The test must compare each fix with the last fix kept, or one spike takes the next good fix with it. Step through it on six fixes:

Interactive · line by line A speed filter drops one spike from six fixes Each fix is compared with the last fix kept. Watch the speed the loop works out at each step, and which fix it leaves out.

Program · spike/spike.py

import math

V_MAX = 30  # m/s
fixes = [  # seconds, east m, north m
    (0, 0, 0),
    (10, 12, 1),
    (20, 25, 3),
    (30, 470, -300),
    (40, 49, 4),
    (50, 61, 5),
]
kept = [fixes[0]]
for t, x, y in fixes[1:]:
    t0, x0, y0 = kept[-1]
    d = math.dist((x, y), (x0, y0))
    speed = d / (t - t0)
    if speed > V_MAX:
        print(f"{t} s: {speed:.1f} m/s, dropped")
        continue
    kept.append((t, x, y))
print(len(kept), "of", len(fixes), "kept")

Variables

After line 21: V_MAX is 30; fixes is [(0, 0, 0), (10, 12, 1), (20, 25, 3), (30, 470, -300), (40, 49, 4), (50, 61, 5)]; kept is [(0, 0, 0), (10, 12, 1), (20, 25, 3), (40, 49, 4), (50, 61, 5)]; t is 50; x is 61; y is 5; t0 is 40; x0 is 49; y0 is 4; d is 12.041594578792296; speed is 1.2041594578792296.V_MAX30fixes(0, 0, 0)0(10, 12, 1)1(20, 25, 3)2(30, 470, -300)3(40, 49, 4)4(50, 61, 5)5kept(0, 0, 0)0(10, 12, 1)1(20, 25, 3)2(40, 49, 4)3(50, 61, 5)4t50x61y5t040x049y04d12.041594578792296speed1.2041594578792296
  • just taken by the loop
  • changed in this step
  • passed by the loop
  • read next
  • where the loop stopped

Printed so far

30 s: 53.8 m/s, dropped
5 of 6 kept

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 38 steps are listed below.

Line run 21
Next line end
Loop pass –
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. import math loads math.
  3. Line 3. V_MAX = 30 sets V_MAX to 30.
  4. Line 4. fixes = [ # seconds, east m, north m (0, 0, 0), (10, 12, 1), (20, 25, 3), (30, 470, -300), (40, 49, 4), (50, 61, 5), ] makes fixes a list of 6 items.
  5. Line 12. kept = [fixes[0]] makes kept a list of 1 item.
  6. Line 13. for t, x, y in fixes[1:]: takes the item at position 0 of fixes[1:]: t is 10, x is 12 and y is 1.
  7. Line 14. t0, x0, y0 = kept[-1] sets t0 to 0, sets x0 to 0 and sets y0 to 0.
  8. Line 15. d = math.dist((x, y), (x0, y0)) sets d to 12.041594578792296.
  9. Line 16. speed = d / (t - t0) sets speed to 1.2041594578792296.
  10. Line 17. speed > V_MAX is false (speed is 1.2041594578792296 and V_MAX is 30), so Python skips the block under if speed > V_MAX:.
  11. Line 20. kept.append((t, x, y)) adds (10, 12, 1) to the end of kept.
  12. Line 13. for t, x, y in fixes[1:]: takes the item at position 1 of fixes[1:]: t is 20, x is 25 and y is 3.
  13. Line 14. t0, x0, y0 = kept[-1] sets t0 to 10 (it was 0), sets x0 to 12 (it was 0) and sets y0 to 1 (it was 0).
  14. Line 15. d = math.dist((x, y), (x0, y0)) sets d to 13.152946437965905 (it was 12.041594578792296).
  15. Line 16. speed = d / (t - t0) sets speed to 1.3152946437965904 (it was 1.2041594578792296).
  16. Line 17. speed > V_MAX is false (speed is 1.3152946437965904 and V_MAX is 30), so Python skips the block under if speed > V_MAX:.
  17. Line 20. kept.append((t, x, y)) adds (20, 25, 3) to the end of kept.
  18. Line 13. for t, x, y in fixes[1:]: takes the item at position 2 of fixes[1:]: t is 30, x is 470 and y is -300.
  19. Line 14. t0, x0, y0 = kept[-1] sets t0 to 20 (it was 10), sets x0 to 25 (it was 12) and sets y0 to 3 (it was 1).
  20. Line 15. d = math.dist((x, y), (x0, y0)) sets d to 538.3623315203247 (it was 13.152946437965905).
  21. Line 16. speed = d / (t - t0) sets speed to 53.83623315203247 (it was 1.3152946437965904).
  22. Line 17. speed > V_MAX is true (speed is 53.83623315203247 and V_MAX is 30), so the block under if speed > V_MAX: runs. Key point The fix at 30 s is 538 m from the last kept fix, 10 s earlier, so reaching it needs 53.8 m/s. Nobody in the field moves that fast, so it is a spike.
  23. Line 18. print(f"{t} s: {speed:.1f} m/s, dropped") prints 30 s: 53.8 m/s, dropped.
  24. Line 19. continue skips the rest of this pass and goes back to the first line of the loop.
  25. Line 13. for t, x, y in fixes[1:]: takes the item at position 3 of fixes[1:]: t is 40, x is 49 and y is 4.
  26. Line 14. t0, x0, y0 = kept[-1] t0 keeps its value, 20, x0 keeps its value, 25 and y0 keeps its value, 3. Key point The fix at 40 s is compared with the last fix kept, from 20 s, not with the spike. 24 m in 20 s is 1.2 m/s, so it stays; measured from the spike it would have looked like 51.9 m/s and been dropped too.
  27. Line 15. d = math.dist((x, y), (x0, y0)) sets d to 24.020824298928627 (it was 538.3623315203247).
  28. Line 16. speed = d / (t - t0) sets speed to 1.2010412149464313 (it was 53.83623315203247).
  29. Line 17. speed > V_MAX is false (speed is 1.2010412149464313 and V_MAX is 30), so Python skips the block under if speed > V_MAX:.
  30. Line 20. kept.append((t, x, y)) adds (40, 49, 4) to the end of kept.
  31. Line 13. for t, x, y in fixes[1:]: takes the item at position 4 of fixes[1:]: t is 50, x is 61 and y is 5.
  32. Line 14. t0, x0, y0 = kept[-1] sets t0 to 40 (it was 20), sets x0 to 49 (it was 25) and sets y0 to 4 (it was 3).
  33. Line 15. d = math.dist((x, y), (x0, y0)) sets d to 12.041594578792296 (it was 24.020824298928627).
  34. Line 16. speed = d / (t - t0) sets speed to 1.2041594578792296 (it was 1.2010412149464313).
  35. Line 17. speed > V_MAX is false (speed is 1.2041594578792296 and V_MAX is 30), so Python skips the block under if speed > V_MAX:.
  36. Line 20. kept.append((t, x, y)) adds (50, 61, 5) to the end of kept.
  37. Line 13. for t, x, y in fixes[1:]: finds no item at position 5 of fixes[1:] (it has 5 items), so the loop ends.
  38. Line 21. print(len(kept), "of", len(fixes), "kept") prints 5 of 6 kept. That was the last line, so the program ends. Five of the six fixes are kept. Comparing each fix with the last one kept is what stops one spike from taking good fixes with it.

The lesson’s field day, 636 fixes 10 s apart, has five planted spikes, two stops, a stretch near tall buildings and an underpass with no signal. MovingPandas offers the same steps as OutlierCleaner, which cuts spikes above a speed (MovingPandas), and TrajectoryStopDetector, which finds stops within a diameter for a duration (MovingPandas); here they are written out:

day.py: the field day as a phone logs it Python · tracks/day.py
"""A synthetic field day as a phone logs it: one fix every 10 s, in metres of UTM zone 43N, with the receiver's
accuracy estimate, planted spikes, two stops with the drift of a receiver standing still, and a tunnel with no fixes.

Each fix is (seconds since 08:00 UTC, east, north, accuracy in m). The true path is known, so cleaning can be checked.
"""
import math
import random

START = (373_500.0, 2_044_500.0)
LEGS = [  # (what, seconds, speed in m/s, heading in degrees clockwise from north)
    ("walk to the first field", 1_200, 1.2, 90),
    ("stop at the field", 1_800, 0.0, 0),
    ("ride north", 900, 10.0, 0),
    ("stop at the market", 1_200, 0.0, 0),
    ("walk back", 1_500, 1.4, 200),
]
TUNNEL = (3_400, 3_640)  # seconds with no fix: an underpass on the ride north
SPIKES = {640: (420.0, -310.0), 2_210: (-650.0, 120.0), 3_250: (300.0, 840.0), 4_120: (-280.0, -520.0), 5_400: (510.0, 380.0)}
BUILDINGS = (4_600, 4_900)  # seconds near tall buildings: poor accuracy, larger errors


def truth():
    """[(t, east, north, leg)] every 10 s along the true path."""
    out, (x, y), t = [], START, 0
    for what, seconds, speed, heading in LEGS:
        for _ in range(seconds // 10):
            out.append((t, x, y, what))
            x += 10 * speed * math.sin(math.radians(heading))
            y += 10 * speed * math.cos(math.radians(heading))
            t += 10
    return out


def logged(seed=11):
    """The fixes the phone logs: truth plus noise, spikes and gaps, each with its accuracy estimate."""
    rng = random.Random(seed)
    out = []
    for t, x, y, _ in truth():
        if TUNNEL[0] <= t < TUNNEL[1]:
            continue
        poor = BUILDINGS[0] <= t < BUILDINGS[1]
        sigma, accuracy = (9.0, round(rng.uniform(18, 30), 1)) if poor else (2.5, round(rng.uniform(3.5, 6.0), 1))
        dx, dy = SPIKES.get(t, (0.0, 0.0))
        out.append((t, x + dx + rng.gauss(0, sigma), y + dy + rng.gauss(0, sigma), accuracy))
    return out


def clock(t):
    """Seconds since 08:00 as a time of day."""
    return f"{8 + t // 3600:02d}:{t // 60 % 60:02d}:{t % 60:02d}"
steps.py: the four cleaning steps Python · tracks/steps.py
"""The four cleaning steps of a GPS track, each a small function over fixes (t, east, north, accuracy)."""
import math


def length(points):
    """The length of a track in metres, fix to fix."""
    return sum(math.dist(a[1:3], b[1:3]) for a, b in zip(points, points[1:]))


def drop_poor(fixes, worst=20.0):
    """Keep the fixes whose accuracy estimate is `worst` metres or better."""
    return [f for f in fixes if f[3] <= worst]


def drop_spikes(fixes, v_max=30.0):
    """Keep a fix only if reaching it from the last kept fix needs no more than v_max m/s; return (kept, dropped)."""
    kept, dropped = [], []
    for f in fixes:
        if kept and math.dist(f[1:3], kept[-1][1:3]) / (f[0] - kept[-1][0]) > v_max:
            dropped.append(f)
        else:
            kept.append(f)
    return kept, dropped


def find_stops(points, diameter=60.0, minimum=300):
    """(first, last) index of every run that stays within `diameter` m of its first fix for `minimum` s or more."""
    out, i = [], 0
    while i < len(points):
        j = i
        while j + 1 < len(points) and math.dist(points[j + 1][1:3], points[i][1:3]) <= diameter / 2:
            j += 1
        if points[j][0] - points[i][0] >= minimum:
            out.append((i, j))
            i = j + 1
        else:
            i += 1
    return out


def split_trips(points, stops, gap=120):
    """Trips between stops, also split where no fix came for more than `gap` s; returns (trips, gap lengths in m)."""
    stopped = {k: (i, j) for i, j in stops for k in range(i, j + 1)}
    trips, current, gaps = [], [], []
    for k, f in enumerate(points):
        if k in stopped:  # a trip ends at a stop's first fix, and the next one starts at its last
            i, j = stopped[k]
            if k == i and current:
                trips.append(current + [f])
                current = []
            if k == j:
                current = [f]
            continue
        if current and f[0] - current[-1][0] > gap:
            trips.append(current)
            gaps.append(math.dist(current[-1][1:3], f[1:3]))
            current = []
        current.append(f)
    if len(current) > 1:
        trips.append(current)
    return trips, gaps
utm.py: UTM zone 43N both ways Python · tracks/utm.py
"""UTM zone 43N on WGS 84, both ways, and the size of a degree on the ground: what the lesson's programs reproject with.

The transverse Mercator projection is computed with Krüger's series to the sixth order of the ellipsoid's third
flattening, as Karney (2011) gives it, which is good to well under a millimetre across the zone.
"""
import math

A, F = 6_378_137.0, 1 / 298.257223563  # WGS 84 semi-major axis (m) and flattening
K0, LON0, FALSE_EASTING = 0.9996, math.radians(75.0), 500_000.0  # zone 43N: central meridian 75° E
E = math.sqrt(F * (2 - F))  # eccentricity
N = F / (2 - F)  # third flattening
RECT = A / (1 + N) * (1 + N**2 / 4 + N**4 / 64 + N**6 / 256)  # the rectifying radius
ALPHA = [
    N / 2 - 2 * N**2 / 3 + 5 * N**3 / 16 + 41 * N**4 / 180 - 127 * N**5 / 288 + 7891 * N**6 / 37800,
    13 * N**2 / 48 - 3 * N**3 / 5 + 557 * N**4 / 1440 + 281 * N**5 / 630 - 1983433 * N**6 / 1935360,
    61 * N**3 / 240 - 103 * N**4 / 140 + 15061 * N**5 / 26880 + 167603 * N**6 / 181440,
    49561 * N**4 / 161280 - 179 * N**5 / 168 + 6601661 * N**6 / 7257600,
    34729 * N**5 / 80640 - 3418889 * N**6 / 1995840,
    212378941 * N**6 / 319334400,
]
BETA = [
    N / 2 - 2 * N**2 / 3 + 37 * N**3 / 96 - N**4 / 360 - 81 * N**5 / 512 + 96199 * N**6 / 604800,
    N**2 / 48 + N**3 / 15 - 437 * N**4 / 1440 + 46 * N**5 / 105 - 1118711 * N**6 / 3870720,
    17 * N**3 / 480 - 37 * N**4 / 840 - 209 * N**5 / 4480 + 5569 * N**6 / 90720,
    4397 * N**4 / 161280 - 11 * N**5 / 504 - 830251 * N**6 / 7257600,
    4583 * N**5 / 161280 - 108847 * N**6 / 3991680,
    20648693 * N**6 / 638668800,
]


def to_utm(lon, lat):
    """Easting and northing (m) in UTM zone 43N of a longitude and latitude in degrees."""
    phi, lam = math.radians(lat), math.radians(lon) - LON0
    t = math.sinh(math.atanh(math.sin(phi)) - E * math.atanh(E * math.sin(phi)))
    xi0, eta0 = math.atan2(t, math.cos(lam)), math.atanh(math.sin(lam) / math.sqrt(1 + t * t))
    xi = xi0 + sum(a * math.sin(2 * j * xi0) * math.cosh(2 * j * eta0) for j, a in enumerate(ALPHA, 1))
    eta = eta0 + sum(a * math.cos(2 * j * xi0) * math.sinh(2 * j * eta0) for j, a in enumerate(ALPHA, 1))
    return FALSE_EASTING + K0 * RECT * eta, K0 * RECT * xi


def from_utm(x, y):
    """Longitude and latitude in degrees of an easting and northing in UTM zone 43N."""
    xi, eta = y / (K0 * RECT), (x - FALSE_EASTING) / (K0 * RECT)
    xi0 = xi - sum(b * math.sin(2 * j * xi) * math.cosh(2 * j * eta) for j, b in enumerate(BETA, 1))
    eta0 = eta - sum(b * math.cos(2 * j * xi) * math.sinh(2 * j * eta) for j, b in enumerate(BETA, 1))
    tau0 = math.sin(xi0) / math.sqrt(math.sinh(eta0) ** 2 + math.cos(xi0) ** 2)
    tau = tau0
    for _ in range(5):  # Newton's method for the conformal latitude
        s = math.sinh(E * math.atanh(E * tau / math.sqrt(1 + tau * tau)))
        guess = tau * math.sqrt(1 + s * s) - s * math.sqrt(1 + tau * tau)
        tau += (tau0 - guess) / math.sqrt(1 + guess * guess) * (1 + (1 - E * E) * tau * tau) / ((1 - E * E) * math.sqrt(1 + tau * tau))
    return math.degrees(LON0 + math.atan2(math.sinh(eta0), math.cos(xi0))), math.degrees(math.atan(tau))


def metres_per_degree(lat):
    """(east-west, north-south) length on the ground of one degree at a latitude, on the ellipsoid."""
    phi = math.radians(lat)
    w = 1 - E * E * math.sin(phi) ** 2
    across = A * math.cos(phi) / math.sqrt(w)  # radius of the parallel
    along = A * (1 - E * E) / w**1.5  # radius of curvature of the meridian
    return math.radians(1) * across, math.radians(1) * along
The field day cleaned, step by step Python · tracks/clean.py
"""Cleaning the field day: drop the fixes the receiver marks as poor, drop spikes by speed, find the stops, and split
the rest into trips at stops and gaps, checked against the true path."""
from day import SPIKES, clock, logged, truth
from steps import drop_poor, drop_spikes, find_stops, length, split_trips

fixes = logged()
print(f"{len(fixes):,} fixes logged, every 10 s, from {clock(fixes[0][0])} to {clock(fixes[-1][0])} UTC")
print(f"true path: {length(truth()) / 1000:.2f} km; the raw fixes add up to {length(fixes) / 1000:.2f} km")

good = drop_poor(fixes)
print(f"accuracy 20 m or better: {len(good):,} fixes kept, {len(fixes) - len(good)} dropped")
kept, spikes = drop_spikes(good)
print(f"faster than 30 m/s from the last kept fix: {len(spikes)} spikes dropped, at "
      f"{', '.join(clock(f[0]) for f in spikes)}; {len(SPIKES)} were planted")

stops = find_stops(kept)
for i, j in stops:
    print(f"stop: {clock(kept[i][0])} to {clock(kept[j][0])}, {j - i + 1} fixes, drifting {length(kept[i : j + 1]):,.0f} m while standing still")
trips, gaps = split_trips(kept, stops)
for t in trips:
    print(f"trip: {clock(t[0][0])} to {clock(t[-1][0])}, {length(t) / 1000:.2f} km, {len(t)} fixes")
moving = sum(map(length, trips))
print(f"trips: {moving / 1000:.2f} km split at the gap; {(moving + sum(gaps)) / 1000:.2f} km with the gap bridged "
      f"by a straight line of {sum(gaps):,.0f} m")

Output

636 fixes logged, every 10 s, from 08:00:00 to 09:49:50 UTC
true path: 12.53 km; the raw fixes add up to 20.47 km
accuracy 20 m or better: 608 fixes kept, 28 dropped
faster than 30 m/s from the last kept fix: 5 spikes dropped, at 08:10:40, 08:36:50, 08:54:10, 09:08:40, 09:30:00; 5 were planted
stop: 08:19:50 to 08:50:00, 181 fixes, drifting 774 m while standing still
stop: 09:05:00 to 09:25:20, 94 fixes, drifting 388 m while standing still
trip: 08:00:00 to 08:19:50, 1.48 km, 119 fixes
trip: 08:50:00 to 08:56:30, 3.90 km, 39 fixes
trip: 09:00:40 to 09:05:00, 2.60 km, 27 fixes
trip: 09:25:20 to 09:49:50, 2.14 km, 147 fixes
trips: 10.12 km split at the gap; 12.63 km with the gap bridged by a straight line of 2,507 m

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 clean.py

The accuracy filter drops the 28 fixes near the buildings, the speed filter finds all five spikes, and the stop detector finds the two stops, whose drift of 774 m and 388 m would otherwise count as distance. The four trips add up to 10.12 km. That is short of the true 12.53 km because the underpass split the ride: bridging its four minutes with a straight line of 2,507 m gives 12.63 km. Whether to bridge a gap is a decision about the road, not the data, so record it with the result.

Measuring a track before cleaning it

The raw fixes add up to 20.47 km for a day of 12.53 km. Summing distances fix to fix counts every spike twice, once out and once back, and every metre of drift while standing still. Drop poor fixes and spikes and take the stops out first, then measure the trips.

Checkpoint

Check: Cleaning a track into trips

  1. Question 1 of 2 A speed filter compares each fix with the fix before it in the log, instead of the last fix it kept. What goes wrong after a spike?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The good fix after the spike also looks too fast, about 51.9 m/s from the spike, and is dropped too

    A spike is a fix in the wrong place, so it must never become the reference for the next one. Compare every fix with the last one kept.

  2. Question 2 of 2 A phone standing still for 30 minutes, one fix every 10 s, logs fixes scattered by about 2.5 m. The lesson's stop at the field added how many metres of false distance?

    Type a number.

    A hint

    The program prints the drift of each stop.

    Show the answer: Question 2 of 2

    Answer: 774 m

    181 fixes, each a few metres from the last, add up to 774 m while nobody moved. Taking the stops out, with the poor fixes and the spikes, is what brought the day from 20.47 km of raw fixes to 10.12 km of trips.

A track is personal data

A track shows where someone sleeps, works and stops. Europe’s GDPR counts “location data” among the identifiers that make information personal data: Information about an identified or identifiable person. The GDPR names location data among the identifiers through which a person can be identified. (GDPR, Article 4). A study of the movements of 1.5 million mobile phone users over 15 months found that four points of place and time singled out 95 % of them (de Montjoye and others). The field day’s two stops, a field and a market, are exactly such points.

Share tracks only as far as the purpose needs:

Purpose Share Leave out
A mileage claim each trip’s length and times the route and the stops
A map of where teams worked fixes counted per grid cell, small counts suppressed every route and every stop
A person’s own record the full track, to that person only nothing

Cut the start and end of every trip near a home, remove the times when they are not needed, and suppress small counts, as the lesson on web map privacy does for cases. Photos taken in the field carry the same risk in their location tags.

Image Metadata Viewer See which location and time a photo from the field carries before you share it.

Checkpoint

Check: Tracks as personal data

  1. One question A manager wants to see where field teams worked last month. What do you share?

    Choose one answer.

    Show the answer: One question

    Answer: Counts of fixes or visits per grid cell, with small counts suppressed

    Location data identifies people, so share the least that answers the question: aggregates for maps, lengths and times for claims, full tracks only to their owners.

Practise: clean a track

Two steps turn this lesson’s cleaning into code you can reuse: drop spikes by speed against the last kept fix, then find the stops in what is left. Each step has sample tests on small tracks:

Code lab · 2 steps

Clean a GPS track

Drop the spikes of a track by speed, then find the stops in what is left.

  1. Drop the spikes of a GPS track by speed To do
  2. Find the stops of a GPS track To do

Exercise · Easy · Python

Drop the spikes of a GPS track by speed

A track is a list of fixes (t, east, north): seconds, and metres in a projected CRS, in time order.

Write drop_spikes(fixes, v_max), which returns (kept, dropped), two lists of fixes. The first fix is always kept. Every later fix is kept if reaching it from the last kept fix needs a speed of v_max metres a second or less, and dropped otherwise.

The sample tests use the lesson's six fixes with one spike, two spikes in a row, and a slower limit for a walker.

Starter code · spikes.py

import math


def drop_spikes(fixes, v_max):
    """(kept, dropped): keep each fix reachable from the last kept fix at v_max m/s or less."""
    # Replace this line: compare every fix with the last kept one.
    return list(fixes), []
The sample tests · test_spikes.py
from spikes import drop_spikes

LESSON = [(0, 0, 0), (10, 12, 1), (20, 25, 3), (30, 470, -300), (40, 49, 4), (50, 61, 5)]


def test_lesson_spike():
    """drops the lesson's spike and keeps the fix after it"""
    kept, dropped = drop_spikes(LESSON, 30)
    assert dropped == [(30, 470, -300)]
    assert len(kept) == 5 and kept[3] == (40, 49, 4)


def test_two_spikes_in_a_row():
    """two spikes in a row are both dropped, and the walk goes on"""
    track = [(0, 0, 0), (10, 10, 0), (20, 900, 0), (30, 905, 2), (40, 30, 0)]
    kept, dropped = drop_spikes(track, 30)
    assert [f[0] for f in dropped] == [20, 30]
    assert [f[0] for f in kept] == [0, 10, 40]


def test_walker_limit():
    """a lower limit for a walker drops a jump that a rider's limit keeps"""
    track = [(0, 0, 0), (10, 15, 0), (20, 160, 0)]
    assert len(drop_spikes(track, 30)[0]) == 3
    assert drop_spikes(track, 3)[1] == [(20, 160, 0)]


def test_first_fix_kept():
    """the first fix is always kept"""
    assert drop_spikes([(0, 5_000, 5_000)], 1) == ([(0, 5_000, 5_000)], [])
A hint

Keep the last kept fix in kept[-1]. The speed to a new fix is math.dist((x, y), (x0, y0)) / (t - t0); compare it with v_max, then append the fix to kept or to dropped.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Exercise · Medium · Python

Find the stops of a GPS track

A track is a list of fixes (t, east, north) in time order: seconds, and metres in a projected CRS.

Write find_stops(fixes, diameter, minimum), which returns a list of (start, end) times, one for each stop. A stop begins at a fix and lasts while the following fixes stay within diameter / 2 metres of that first fix; it counts only if it lasts minimum seconds or more, from its first fix to its last. After a stop, look for the next one from the fix after it; after a run too short to be a stop, try again from the next fix.

The sample tests use a walk with one stop of 6 minutes with drift and a pause of 2 minutes that is not a stop, a diameter too small for the drift, and a slow drift whose stops must follow one another without overlapping.

Starter code · stops.py

import math


def find_stops(fixes, diameter, minimum):
    """[(start, end)] of every run that stays within diameter / 2 m of its first fix for minimum s or more."""
    # Replace this line: grow a run from each fix while the fixes stay close to its first one.
    return []
The sample tests · test_stops.py
from stops import find_stops


def walk(t0, x0, seconds, speed):
    """Fixes every 10 s of a walk east."""
    return [(t0 + k, x0 + speed * k, 0.0) for k in range(0, seconds, 10)]


def stay(t0, x0, seconds, drift):
    """Fixes every 10 s standing near x0, drifting by up to `drift` m."""
    return [(t0 + k, x0 + (drift if k % 20 else -drift), drift / 2) for k in range(0, seconds, 10)]


TRACK = walk(0, 0, 300, 1.4) + stay(300, 420, 360, 8) + walk(660, 420, 300, 1.4) + stay(960, 840, 120, 5) + walk(1080, 840, 200, 1.4)


def test_one_stop():
    """finds the 6-minute stop, with the walk's fixes just before and after it, and not the 2-minute pause"""
    assert find_stops(TRACK, 60, 300) == [(290, 670)]


def test_short_minimum():
    """with a minimum of 100 s the pause counts too"""
    assert find_stops(TRACK, 60, 100) == [(290, 670), (950, 1090)]


def test_tight_diameter():
    """a diameter smaller than the drift finds no stop"""
    assert find_stops(TRACK, 10, 300) == []


def test_empty():
    """an empty track has no stops"""
    assert find_stops([], 60, 300) == []


def test_next_stop_starts_after_the_last():
    """after a stop the search goes on from the next fix, so a slow drift gives stops that never overlap"""
    drift = [(10 * k, 5.0 * k, 0.0) for k in range(20)]
    assert find_stops(drift, 60, 60) == [(0, 60), (70, 130)]
A hint

Use two indices: i for the fix a stop would start at, and j moving forward while math.dist(fixes[j + 1][1:], fixes[i][1:]) <= diameter / 2. If fixes[j][0] - fixes[i][0] >= minimum, record the stop and jump to j + 1; if not, move i on by one.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

The self-check below covers all six ideas, from the four unknowns to sharing a track, and starts with the ones your answers have been weakest on:

Practice that adapts to you

Practise this lesson

11 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 11 What is a pseudorange?

    Choose one answer.

    Show the answer: Question 1 of 11

    Answer: A satellite's range as the receiver measures it, including its own clock error

    Travel time times the speed of light, measured by an imperfect clock: a 1 ms error is 299,792 m on every range, which is why the clock is solved with the position.

  2. Question 2 of 11 A survey can be done in the morning, with satellites spread round the sky, or in the afternoon, with most of them low in the east. The receiver and site are the same. Which do you choose, and why?

    Choose one answer.

    Show the answer: Question 2 of 11

    Answer: The morning, because spread satellites give a lower dilution of precision for the same range errors

    Planning tools show the dilution of precision over a day. Survey when it is low, and avoid times when the satellites crowd into one part of the sky.

  3. Question 3 of 11 How accurate does GPS.gov say GPS phones typically are under open sky?

    Choose one answer.

    Show the answer: Question 3 of 11

    Answer: Within a 4.9 m radius

    The signal-in-space error is small; local conditions and the receiver decide most of what a phone reaches, about 5 m in the open and worse among buildings.

  4. Question 4 of 11 An Indian Entity must mark plot corners to within 5 cm and know each position while standing on it. Which method fits?

    Choose one answer.

    Show the answer: Question 4 of 11

    Answer: Network RTK from Survey of India's CORS

    Centimetres need corrections; live centimetres need them in real time. The trade-off table rules out the others for exactly these reasons.

  5. Question 5 of 11 In which bands does NavIC broadcast, and what did its newer satellites add?

    Choose one answer.

    Show the answer: Question 5 of 11

    Answer: L5 and S band, with a civilian signal in L1 added on satellites from 2023 on

    L1 is the band most phone chips already receive for GPS, which is why a civilian NavIC signal there matters.

  6. Question 6 of 11 Which services does Survey of India's CORS page list procedures for?

    Choose one answer.

    Show the answer: Question 6 of 11

    Answer: Network RTK and DGNSS surveys, online post-processing and data download

    CORS are reference stations on the ground. Their data corrects receivers live, as network RTK, or afterwards, as post-processing.

  7. Question 7 of 11 Why record @position_averaged_count with a point that QField averaged from several fixes?

    Choose one answer.

    Show the answer: Question 7 of 11

    Answer: It says how many fixes made the point, 0 when it was not averaged, so its quality can be judged later

    Field data is judged later, by someone who was not there. Every number that describes how a position was made belongs with it.

  8. Question 8 of 11 Which attributes must the root element of a GPX 1.1 file carry?

    Choose one answer.

    Show the answer: Question 8 of 11

    Answer: version="1.1" and creator

    A GPX file names its version and the program that wrote it; every point then carries lat and lon in WGS 84 degrees.

  9. Question 9 of 11 The cleaned trips add up to 10.12 km, the true day was 12.53 km, and bridging the underpass with a straight line gives 12.63 km. What do you report?

    Choose one answer.

    Show the answer: Question 9 of 11

    Answer: 12.63 km with the gap bridged and the bridge stated, if the road through the underpass is straight

    A gap is a decision, not an error: bridge it when the path is known to be direct, leave it when it is not, and record which.

  10. Question 10 of 11 How does the lesson find a stop?

    Choose one answer.

    Show the answer: Question 10 of 11

    Answer: A run of fixes that stays within half a 60 m diameter of its first fix for at least 5 minutes

    Drift makes a standing receiver look as if it moves, so a stop is found by a small area and a long time together.

  11. Question 11 of 11 What did the study of 15 months of mobility data for 1.5 million people find?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: Four points of place and time were enough to single out 95 % of them

    That is why removing names from tracks does not make them anonymous, and why aggregates are the safe default.

Interview questions

Warm-up (fresher): Why does a GNSS receiver need at least four satellites for a position?

Each satellite gives one range, but the receiver’s clock is not exact, so every range is off by the same unknown amount: 1 ms of clock error is 299,792 m. The receiver must solve its east, north and up position and that clock error together, four unknowns, which needs four ranges at least. More satellites let it solve by least squares, and the more spread they are over the sky, the less their range errors move the fix: with the same 3 m errors, 95 % of the lesson’s fixes were within 6.2 m with spread satellites and within 73.2 m with crowded ones.

Warm-up (fresher to mid-level): A phone says a point’s accuracy is 5 m. What should you store with the point?

The accuracy itself, in metres, as the receiver reported it, with the time of the fix, the number of satellites used and how many fixes were averaged. In QField those are the default values @position_horizontal_accuracy, @position_timestamp, @position_number_of_used_satellites and @position_averaged_count. Without them nobody can later tell a careful point from a quick one, or decide whether two points 4 m apart are the same place.

Two longer questions start from this lesson, from pillars that move between visits to riders’ distances that come out too long; the bank gives what each tests and a first step:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    Field staff recorded the same 40 boundary pillars with their phones on two days, and the coordinates differ by 3 to 12 m. Which day is right, and how should the pillars be recorded so that the next visit agrees?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    Delivery riders' GPS logs give daily distances about 40 % above their odometers, and some trips show jumps across the city in ten seconds. Explain both, and design the cleaning that makes the distances usable for paying riders.

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Why does a GNSS receiver need at least four satellites for a fix?

    Its clock is cheap, and 1 ms of clock error adds 299,792 m to every range, so each range is a pseudorange. The receiver solves east, north, up and its own clock error together: four unknowns need at least four ranges.

  2. What is dilution of precision, and how much can satellite geometry matter?

    The factor that turns range errors into position errors, set by the satellites' geometry alone. With 3 m range errors, six satellites spread over the sky put 95 % of fixes within 6.2 m (HDOP 1.2); the same six crowded into one part of the sky, within 73.2 m (HDOP 12.6).

  3. How do reference stations improve a GNSS position, and what can they not fix?

    A station at a known position measures what its ranges should be, and the differences, sent live as in RTK or applied afterwards, cancel most of the satellite clock, orbit and atmosphere errors a nearby receiver shares, down to centimetres. No correction restores a signal a wall or a canopy blocks.

  4. What is NavIC?

    India's regional navigation system: 7 satellites, three geostationary and four in inclined geosynchronous orbits, covering India and up to 1,500 km beyond, with a design accuracy better than 20 m in its primary service area. A phone that adds NavIC to GPS sees more satellites, which lowers the dilution of precision.

  5. What does Survey of India's CORS network offer, and who may use its data?

    Continuously Operating Reference Stations for network RTK and DGNSS surveys, online post-processing and data download, after registration. India's 2021 guidelines make CORS data available without restrictions, with the ease of access for Indian Entities only.

  6. What should be stored with every field point, and where?

    Its horizontal and vertical accuracy, the satellites used, the fix quality, the number of averaged fixes and the time, filled in QField by variables such as @position_horizontal_accuracy and @position_timestamp. GPX has hdop and sat but no accuracy in metres, so keep them in the layer, such as a GeoPackage.

  7. Why did a 12.53 km field day log as 20.47 km, and how is the track cleaned?

    Summing fix to fix counts each spike twice, out and back, and every metre of drift while standing still. Drop the fixes the receiver marks as poor, drop spikes by testing speed against the last fix kept, take out the stops, then measure the trips and record how gaps were bridged.

  8. Why is a GPS track personal data, and how much of it should be shared?

    It shows where someone sleeps, works and stops, and in one study four points of place and time singled out 95 % of 1.5 million phone users. Share only what the purpose needs: trip lengths and times for a mileage claim, counts per grid cell with small counts suppressed for a map of where teams worked.

Key takeaways

  • A receiver solves four unknowns, its position and its clock error, from four or more pseudoranges, and the satellites’ geometry scales range errors into position errors.
  • Errors come from blocked and reflected signals, the atmosphere and the receiver; reference stations remove the errors a nearby receiver shares, down to centimetres.
  • NavIC covers India and 1,500 km beyond with a design accuracy better than 20 m, and Survey of India’s CORS data is available to Indian Entities.
  • Store the accuracy, time and satellites of every field point with it; GPX carries tracks, one segment per continuous span.
  • Clean a track before measuring it: drop poor fixes and spikes against the last kept fix, take out the stops, and record how gaps were treated.
  • A track is personal data: share only what the purpose needs, aggregated where you can.

Terms in this lesson

CORS (Continuously Operating Reference Stations)
GNSS receivers fixed at precisely known positions that record without a break, so that their data can correct other receivers, in real time or afterwards.
DOP (dilution of precision)
A number from the satellites' geometry that multiplies range errors into position errors: low when the satellites are spread over the sky, high when they crowd into one part of it.
GNSS (global navigation satellite system)
A system of satellites that broadcast their time and orbit so that a receiver can work out its position and clock: GPS, NavIC, Galileo and BeiDou are examples.
GPX
An XML format for GPS data: waypoints, routes and tracks, a track made of segments of points with latitude and longitude in WGS 84 and an optional time.
NavIC
India's regional navigation satellite system, also called IRNSS: a constellation designed as seven satellites serving India and the region up to 1,500 km beyond it.
pseudorange
A receiver's measured distance to a satellite, the signal's travel time times the speed of light, which includes the error of the receiver's own clock.
RTK (real-time kinematic positioning)
Positioning in which a receiver applies corrections from a reference station, or a network of them, as it measures, reaching centimetres while the corrections arrive.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.