"I do not measure where you are. I measure how long light took to reach you from four clocks in the sky, and I subtract the lies each one told."
A Trilaterating AI Agent
Prerequisites
This section assumes the time-of-flight and clock-synchronization ideas from Chapter 3: a position fix is fundamentally a timing measurement, and a nanosecond of clock error is about thirty centimetres of range error. It also leans on the measurement-model and uncertainty vocabulary of Chapter 4, since every error source below is a bias or a variance we must model rather than wish away. Linear algebra and least squares from the Part I math reference are enough; no prior knowledge of satellite navigation is assumed. Python with NumPy suffices for the code.
The Big Picture
A GNSS receiver is a clock comparator that happens to output coordinates. It listens to several satellites, each broadcasting the time it thinks it is, computes how long each signal took to arrive, multiplies by the speed of light to get a range, and intersects those ranges to find itself. That is the whole idea. Everything hard about the field, and everything that makes a raw phone fix wander by ten metres in a city, lives in the gap between that clean geometric story and the messy physics of clocks, atmospheres, and reflections. This section builds the clean story first, then dismantles it error source by error source, because a sensory-AI system that treats GNSS as ground truth will be misled exactly where cities, canyons, and tunnels make it least reliable. The fusion that repairs this (with inertial dead reckoning from Chapter 24) is the subject of the rest of the chapter; here we earn the right to distrust the number.
Constellations, signals, and what a receiver actually hears
GNSS, Global Navigation Satellite System, is the umbrella term for four independent constellations: the United States' GPS, Russia's GLONASS, the European Union's Galileo, and China's BeiDou. Each flies roughly 24 to 30 satellites in medium Earth orbit near 20,000 km, arranged so that at least four are above the horizon almost anywhere on Earth at any time. A modern receiver chip is multi-constellation: it does not care whose satellite it is, only that the satellite broadcasts its position and a precise time. Each satellite carries an atomic clock and continuously transmits a signal encoding two things: the exact time of transmission, and the ephemeris, a compact orbital model from which the receiver computes where that satellite was at that instant. Knowing where the transmitter was and when it transmitted is half of a range measurement; the receiver supplies the other half by noting when the signal arrived.
The measured quantity is not a true range but a pseudorange. The receiver's own clock is a cheap quartz oscillator, not an atomic standard, so it is offset from GNSS system time by an unknown bias \(b\). The distance it infers from time of flight is therefore \[ \rho_i = \lVert \mathbf{x} - \mathbf{s}_i \rVert + c\,b + \varepsilon_i, \] where \(\mathbf{x}\) is the unknown receiver position, \(\mathbf{s}_i\) the known position of satellite \(i\), \(c\) the speed of light, and \(\varepsilon_i\) lumps together every error we will catalogue below. The clock bias \(b\) is shared across all satellites in a given epoch, which is the crucial structural fact: it is a fourth unknown alongside the three coordinates of \(\mathbf{x}\), so a receiver needs at least four satellites to solve for four unknowns. Three would suffice for pure trilateration if the clock were perfect; the fourth satellite pays for the luxury of a cheap clock.
Key Insight
The receiver clock bias is not a nuisance to be minimized, it is a solved-for state variable, and that changes how you should think about GNSS. Because \(b\) is estimated jointly with position at every epoch, a GNSS receiver effectively disciplines its own cheap clock to atomic accuracy as a free by-product of positioning, which is why GNSS is the backbone of timing for power grids, cellular networks, and financial timestamping. It also explains the four-satellite minimum in one line: three unknowns of position plus one of time. When you later fuse GNSS with an inertial navigation system in Section 25.5, you carry this clock bias (and its drift) as part of the fused state, not as noise to be filtered out.
Solving the fix: nonlinear least squares
The pseudorange equation is nonlinear in \(\mathbf{x}\) because of the Euclidean norm, so we cannot invert it directly. The standard solution linearizes about a current guess and iterates, a Gauss-Newton method. Stack the residuals \(r_i = \rho_i - (\lVert \mathbf{x} - \mathbf{s}_i \rVert + c\,b)\) and their Jacobian with respect to the four unknowns \([\mathbf{x}, b]\); each row of the Jacobian is the unit line-of-sight vector from receiver to satellite, followed by \(c\). Solving the linear least-squares update and repeating converges in three or four iterations. The same geometry that drives that Jacobian also governs how measurement noise inflates into position error, a factor called dilution of precision, developed below. The code makes the whole loop concrete.
import numpy as np
def solve_position(sats, pseudoranges, iters=6):
"""Estimate [x, y, z, c*b] from satellite ECEF positions and pseudoranges.
sats: (N, 3) satellite positions (m); pseudoranges: (N,) measured ranges (m)."""
x = np.zeros(4) # start at Earth's centre, clock bias 0
for _ in range(iters):
diff = x[:3] - sats # (N, 3) receiver-minus-satellite
ranges = np.linalg.norm(diff, axis=1)
predicted = ranges + x[3] # geometric range + clock-bias term
residual = pseudoranges - predicted # (N,)
los = diff / ranges[:, None] # unit line-of-sight vectors
H = np.hstack([los, np.ones((len(sats), 1))]) # (N, 4) Jacobian
dx, *_ = np.linalg.lstsq(H, residual, rcond=None)
x = x + dx
if np.linalg.norm(dx) < 1e-4:
break
return x[:3], x[3] # position (m), clock bias * c (m)
# Four satellites (toy ECEF coords); true position near Earth's surface.
sats = np.array([[15600e3, 7540e3, 20140e3], [18760e3, 2750e3, 18610e3],
[17610e3, 14630e3, 13480e3], [19170e3, 610e3, 18390e3]])
true_pos = np.array([-2694e3, -4293e3, 3857e3]); true_bias = 85.0 # metres
pr = np.linalg.norm(true_pos - sats, axis=1) + true_bias # clean pseudoranges
est_pos, est_bias = solve_position(sats, pr)
print(f"position error = {np.linalg.norm(est_pos - true_pos):.3f} m")
print(f"clock bias est = {est_bias:.2f} m (true {true_bias})")
With perfect pseudoranges, as printed, the fix recovers position to numerical precision and reads the clock bias back exactly. Real pseudoranges are never clean, and adding realistic per-satellite errors to pr above turns that sub-millimetre number into metres. The rest of the section is a tour of what fills \(\varepsilon_i\), because a sensory-AI pipeline must know which errors are slowly varying biases it can correct and which are unpredictable spikes it must reject.
The error budget: where the metres come from
The total ranging error is conventionally decomposed into a per-satellite user-equivalent range error (UERE), then amplified by geometry. Taking the sources in order of typical magnitude: the ionosphere, a layer of charged particles from about 50 to 1000 km up, slows the signal's code and speeds its phase, contributing up to several metres of delay that varies with solar activity, time of day, and elevation angle. Because this delay is frequency-dependent, a dual-frequency receiver measuring the same satellite on two bands can solve for and remove most of it, the single biggest reason modern multi-band phone chips fix so much better than the single-frequency chips of a decade ago. The troposphere, the lowest 10 to 15 km, adds a further delay from water vapour and dry air that is frequency-independent (so it cannot be cancelled that way) but is smooth and well modelled from elevation angle and a standard atmosphere, leaving a residual under a metre. Satellite clock and ephemeris errors are the small remaining mismatches between where the broadcast says a satellite is and when its clock says it is versus reality; the ground control segment keeps these to a metre or two, and precise correction services drive them to centimetres.
Two error sources behave differently because they are local to the receiver's environment and therefore uncorrelated with anything a satellite or a wide-area model can fix. Multipath is the signal arriving both directly and via reflections off buildings, water, or the ground; the receiver correlates a blurred sum and mismeasures the range, adding anywhere from centimetres in the open to tens of metres in an urban canyon. Receiver thermal noise is the irreducible jitter of the tracking loops, typically a few decimetres. Multipath is the villain of city navigation precisely because it is not Gaussian: it produces sudden, biased, non-symmetric errors that violate the assumptions of the least-squares solver above and of the Kalman filters in Chapter 9, which is why robust or fault-detecting estimators earn their keep here.
In Practice: the rideshare app that put the pickup on the wrong street
A rideshare platform saw a cluster of complaints in one downtown district: the app repeatedly placed riders and drivers a full block away, on the parallel avenue, so cars circled and trips started late. The phones were not broken. Between two rows of glass towers, the direct signals from low-elevation satellites were blocked, and the receiver was tracking their reflections bouncing off the opposite facade, a multipath bias of 30 to 40 metres that pushed the least-squares fix sideways onto the next street. The single-frequency fix could not detect it, and the geometry was poor because only satellites near the strip of visible sky overhead contributed. The eventual mitigation combined three things this chapter builds toward: rejecting low-elevation satellites, snapping the fix to the road network (map matching, Section 25.4), and fusing with the phone's inertial dead reckoning so a physically impossible sideways jump was down-weighted. No single fix sufficed; the lesson is that urban GNSS error is structural, not random, and must be attacked as such.
Geometry, corrections, and what you can actually expect
Even with perfectly measured pseudoranges, the position error depends on where the satellites are, not just how well each is measured. If all visible satellites cluster in one patch of sky, their line-of-sight vectors are nearly parallel, the least-squares problem is ill-conditioned, and small ranging errors blow up into large position errors. This amplification is geometric dilution of precision (GDOP), and it is computed directly from the same Jacobian \(H\) the solver already builds: the position error covariance is \(\sigma^2 (H^\top H)^{-1}\), and the DOP values are the square roots of its diagonal. A GDOP near 1 is excellent (satellites well spread); a GDOP above 6 means a metre of ranging error becomes six metres of position error. Wide sky helps, canyons and tree cover hurt, and a good receiver reports DOP so downstream code can weight the fix, a habit that connects directly to the localization-quality metrics of Section 25.6.
Putting numbers to it: an unaugmented single-frequency receiver in the open sky delivers roughly 3 to 5 metres of horizontal error (95th percentile). Augmentation services close the gap. SBAS (WAAS, EGNOS, and peers) broadcasts wide-area corrections and integrity flags from geostationary satellites, reaching 1 to 2 metres. Differential GNSS and RTK (real-time kinematic) use a nearby reference station of known position to cancel the spatially correlated errors (ionosphere, troposphere, satellite clock/ephemeris) that the two receivers share, and RTK's use of the carrier phase reaches centimetre accuracy, which is what surveying, precision agriculture, and automated vehicles rely on. PPP (precise point positioning) achieves similar accuracy globally without a local base, at the cost of a long convergence time. The pattern is consistent: corrections work by cancelling the errors that are common to nearby receivers, leaving only the local ones, multipath and receiver noise, that no external service can remove for you.
Right Tool: georinc and gnss-lib for real receiver data
The least-squares solver above is the pedagogical core, but a production pipeline must parse RINEX observation and navigation files, compute satellite positions from broadcast ephemerides (a page of Keplerian orbital mechanics with relativistic corrections), apply the Klobuchar ionosphere and Saastamoinen troposphere models, and weight by elevation, easily 500 to 800 lines of careful, standards-conformant code. Open GNSS toolkits such as georinest/gnss_lib_py reduce reading a day of dual-frequency observations and producing weighted least-squares fixes with DOP to roughly a dozen calls. They handle the RINEX parsing, the ephemeris propagation, and the standard atmospheric models that would otherwise dominate your codebase. Implement the four-satellite solver once to understand the geometry, then let the library carry the standards compliance in production.
Exercise
Extend the code above. (1) Add independent Gaussian noise of standard deviation 3 m to each pseudorange and run the fix 1000 times; report the horizontal error distribution and compare its spread to the DOP prediction \(\sigma\sqrt{\operatorname{trace}(H^\top H)^{-1}_{\text{horizontal}}}\). (2) Move all four satellites into a narrow overhead cone and repeat; show that GDOP and the empirical error both explode. (3) Add a 30 m biased error to one satellite (a multipath spike) and show that plain least squares smears it across the fix, then implement a simple residual-threshold rejection that detects and drops the faulty satellite when a fifth is available.
Self-Check
1. Why does a GNSS receiver need four satellites rather than three, and what is the fourth unknown? 2. Which error sources can a dual-frequency receiver cancel on its own, and which require an external reference station or model, and why does that distinction fall along the line of frequency dependence? 3. A receiver reports a clean fix with a GDOP of 9. What does that tell you about the satellite geometry, and how should a downstream fusion filter treat that epoch?
What's Next
In Section 25.2, we come indoors, where the satellite signals this section relied on are blocked by walls and the whole trilateration story must be rebuilt from terrestrial radios. Wi-Fi, Bluetooth, and ultra-wideband beacons play the role the satellites played here, but with new twists: no atomic clocks, far shorter ranges, and severe multipath in reflective rooms. The pseudorange and dilution-of-precision intuitions you now hold carry directly across, and set up the geometric-versus-fingerprinting contrast of Section 25.3.