Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Carrier-Phase GNSS: RTK and PPP-RTK

Open In Colab

Overview

Carrier-phase GNSS reaches mm-cm accuracy once the integer ambiguity of each phase measurement is resolved. This notebook shows the two classic flavours, both as GTSAM factor graphs solved with incremental ISAM2 and fixed with LAMBDA:

  • Part 1 - RTK (double difference): a nearby base station differences away the clocks and atmosphere. Factors: DoubleDifferencePseudorangeFactor, DoubleDifferenceCarrierPhaseFactor.

  • Part 2 - PPP-RTK (undifferenced): no base station; the receiver clock, tropospheric ZTD and slant ionosphere become state variables, corrected with QZSS CLAS. Factors: UndifferencedPseudorangeFactor, UndifferencedCarrierPhaseFactor.

All the GNSS plumbing (RINEX / CLAS decoding, satellite states, observation extraction, and the LAMBDA covariance bridge) lives in the helper module gnss_frontend.py so the cells below show only the factor graph.

GTSAM Copyright 2010-2026, Georgia Tech Research Corporation, Atlanta, Georgia 30332-0415 All Rights Reserved

Authors: Frank Dellaert, et al. (see THANKS for the full author list)

See LICENSE for the license information

data ready in /home/ubuntu/cssrlib/cssrlib-data/data

Part 1 - RTK (double difference, base + rover)

A static rover and a base at a surveyed location see the same satellites. Differencing between receivers and between satellites cancels the clocks and most of the atmosphere, leaving the rover position and the between-receiver integer ambiguities. gnss_frontend.load_rtk returns the per-epoch double-difference bundles (bundled data, no download).

59 epochs;  reference satellite per system: {<uGNSS.GPS: 0>: 17, <uGNSS.GAL: 1>: 45}

Build the graph incrementally. For each epoch we add, per double difference, a DoubleDifferencePseudorangeFactor and a DoubleDifferenceCarrierPhaseFactor (each forms the rover-base double difference internally). Only between-satellite differences are observable, so one reference ambiguity per constellation is unobservable -- we gauge-pin it with a prior. ISAM2 uses QR factorization for robust marginals.

float  2D=0.155  3D=0.262 m

Resolve the integers. The ambiguity state N is represented as a continuous double (in cycles) in the GTSAM factor graph, so ISAM2 returns the float ambiguity estimate -- GTSAM never optimizes N as an integer. Integer ambiguity fixing is a separate step, performed here with the LAMBDA method using the float estimate and its covariance. gnss_frontend.resolve_integer_ambiguities writes the float ambiguities and their joint covariance into the front-end nav state and runs the LAMBDA search (resamb_lambda). With a short baseline RTK fixes to the mm-cm level.

float  2D=0.155  3D=0.262 m
FIXED  2D=0.007  3D=0.015 m  (28 SD ambiguities)

Part 2 - PPP-RTK (undifferenced, single receiver + CLAS)

Now drop the base station. Without differencing, the receiver clock, tropospheric zenith wet delay (ZTD) and slant ionosphere no longer cancel, so they become state variables (corrected by QZSS CLAS). load_ppp decodes CLAS and returns the per-epoch undifferenced observations.

100 epochs;  constellations (<uGNSS.GPS: 0>, <uGNSS.GAL: 1>, <uGNSS.QZS: 2>)

Build the graph. Each satellite/frequency adds an UndifferencedPseudorangeFactor and an UndifferencedCarrierPhaseFactor, wired to the position, the system clock, the ZTD and the slant iono (and, for carrier, the ambiguity). The clock is loosely-priored, the iono gets a tight CLAS prior, and ZTD is linked across epochs by the random-walk factor.

float  2D=0.203  3D=0.217 m

Resolve the integers, exactly as in RTK: the graph gives the float ambiguities and LAMBDA fixes them afterwards. Unlike RTK, the PPP position is co-estimated with the clock/ZTD/iono and only becomes observable after a few epochs, so resolve_integer_ambiguities takes a convergence gate conv_sigma=1.0 m before attempting integer fixing.

float  2D=0.203  3D=0.217 m
FIXED  2D=0.022  3D=0.074 m  (22 SD ambiguities)

Sources