navigation: add undifferenced (PPP) pseudorange and carrier-phase factors#2575
navigation: add undifferenced (PPP) pseudorange and carrier-phase factors#2575inuex35 wants to merge 13 commits into
Conversation
First piece of the uncombined PPP factor family (Step 2). Models a single raw pseudorange (SSR satellite corrections pre-applied) with the receiver clock, zenith wet tropo and slant ionosphere carried as state variables: error = geodist(sat, rcv) + c*(dt_u - dt_s) + m_w*ZTD + mu_f*I_slant - meas Geometry reuses the Sagnac-corrected gnss::geodist primitive. The wet mapping function m_w and iono coefficient mu_f are held constant per factor. Keys: [pos, clock, ztd, slant-iono]. Adds a test validating the residual and all four analytic Jacobians against numericalDerivative. Follow-ups: CarrierPhase counterpart (adds ambiguity), Arm (Pose3) variants, navigation.i bindings, and a cssrlib pppssr residual cross-check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the Arm (Pose3 + lever arm) and carrier-phase members of the uncombined PPP factor family, plus Python bindings for all four: - UncombinedPseudorangeFactorArm [pose, clock, ztd, iono] - UncombinedCarrierPhaseFactor [pos, clock, ztd, iono, N] - UncombinedCarrierPhaseFactorArm [pose, clock, ztd, iono, N] Carrier phase advances with the ionosphere (-mu_f*I) and carries the integer ambiguity in cycles with a lambda coefficient (+lambda*N), preserving the integer structure for ambiguity resolution. Arm variants reuse gnss::LeverArm (optional ecef_T_nav). All geometry routes through Sagnac-corrected gnss::geodist. Tests: residual + numerical-Jacobian checks for every factor and an ecef_T_nav consistency check for the CP Arm variant. navigation.i exposes the four factors to Python. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Align naming with the differencing-scheme family already in navigation: Undifferenced (PPP) <-> Differential (SD) <-> DoubleDifference (DD/RTK) "Undifferenced" is the textbook counterpart to DoubleDifference and avoids the "uncombined" term, which implies an ionosphere-free counterpart that does not exist. Pure rename: UncombinedPseudorangeFactor(Arm) -> UndifferencedPseudorangeFactor(Arm) and the CarrierPhase equivalents, across headers, impl, navigation.i bindings and tests. Behaviour unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The undifferenced PPP factors serialized their base object via BOOST_SERIALIZATION_BASE_OBJECT_NVP(<Class>::Base), whose NVP tag contains "::" and is rejected by the XML archive on a round-trip. Use BOOST_SERIALIZATION_BASE_OBJECT_NVP(Base) (the in-class typedef, the dominant convention) so the tag is XML-valid. Add obj/XML/binary round-trip tests to testSerializationNavigation for UndifferencedPseudorangeFactor / UndifferencedCarrierPhaseFactor and their lever-arm variants. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds new undifferenced (PPP-style) GNSS measurement factors to the gtsam/navigation module, enabling PPP estimation by explicitly including receiver clock bias, tropospheric zenith wet delay, and slant ionosphere (and carrier-phase ambiguity) as state variables. The new factors compute geometry using gnss::geodist (Sagnac-corrected) and include analytic Jacobians, SWIG/Python exposure, and unit tests with numerical Jacobian checks.
Changes:
- Add
UndifferencedPseudorangeFactor(+Arm) with state keys[pos/pose, clock, ZTD_wet, slant-iono]and Sagnac-corrected geometry viagnss::geodist. - Add
UndifferencedCarrierPhaseFactor(+Arm) with state keys[pos/pose, clock, ZTD_wet, slant-iono, ambiguity], including the+ lambda * Nterm and opposite-sign ionosphere term vs code. - Add unit tests (model + Jacobians) and SWIG interface entries to expose the new factors to wrappers.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| gtsam/navigation/PseudorangeFactor.h | Declares new undifferenced PPP pseudorange factors (point and lever-arm variants) with doxygen docs and serialization. |
| gtsam/navigation/PseudorangeFactor.cpp | Implements residuals/Jacobians for the new pseudorange PPP factors using gnss::geodist and lever-arm chaining. |
| gtsam/navigation/CarrierPhaseFactor.h | Declares new undifferenced PPP carrier-phase factors (point and lever-arm variants), including wavelength-scaled ambiguity handling. |
| gtsam/navigation/CarrierPhaseFactor.cpp | Implements residuals/Jacobians for the new carrier-phase PPP factors (including iono sign and ambiguity scaling) using gnss::geodist. |
| gtsam/navigation/navigation.i | Exposes the new factor classes/constructors and evaluateError methods to the wrapper interface. |
| gtsam/navigation/tests/testPseudorangeFactor.cpp | Adds PPP pseudorange unit tests validating the measurement model and Jacobians (including Arm variant). |
| gtsam/navigation/tests/testCarrierPhaseFactor.cpp | Adds PPP carrier-phase unit tests validating the measurement model, Jacobians (including Arm), and ecef_T_nav consistency. |
|
Would it be possible to add a notebook example before I review? |
|
Sure — I can add a short open-sky notebook example. A clean open-sky case avoids the multipath/cycle-slip complications, so it should make the factors easy to follow. I'll push it shortly. |
Demonstrates the new undifferenced factors (UndifferencedPseudorangeFactor, UndifferencedCarrierPhaseFactor) on a real open-sky QZSS CLAS record: a single receiver (no base station) reaches cm-level accuracy. The cssrlib GNSS front-end decodes CLAS and produces SSR-corrected undifferenced residuals; GTSAM estimates the static position together with per-epoch clock, ZTD (random walk) and slant-iono states plus float ambiguities via incremental ISAM2, and the integers are resolved with LAMBDA. Follows the existing GNSS example pattern (SinglePointPositioning, DifferentialPseudorange): Colab badge, license cell, pip-install of the front-end, and runtime data download. Executed result: float 2D=0.21 m -> fixed 2D=0.022 m (22 SD ambiguities). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fferenced-factors # Conflicts: # gtsam/navigation/tests/testPseudorangeFactor.cpp # gtsam/navigation/tests/testSerializationNavigation.cpp
…ifferenced-notebook
Replace PrecisePointPositioningExample.ipynb with RtkAndPppExample.ipynb,
which now covers both flavours of carrier-phase integer ambiguity
resolution in one story:
- Part 1 - RTK (double difference): base + rover, DoubleDifference
{Pseudorange,CarrierPhase}Factor, on the dataset bundled with cssrlib
(no download). float 2D=0.155 m -> fixed 2D=0.007 m (28 SD ambiguities).
- Part 2 - PPP-RTK (undifferenced): single receiver + QZSS CLAS,
Undifferenced{Pseudorange,CarrierPhase}Factor. float 2D=0.208 m ->
fixed 2D=0.022 m (22 SD ambiguities).
Both build the float graph with incremental ISAM2 and resolve integers
with LAMBDA. cssrlib provides the GNSS front-end.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make RtkAndPppExample.ipynb readable by hiding the cssrlib front-end (RINEX/CLAS decoding, satellite states, observation extraction) and the LAMBDA covariance bridge in a helper module, mirroring how gnss_utils.py backs the pyrtklib examples. The notebook cells now show only the GTSAM factor graph: load -> add Double-Difference / Undifferenced factors -> ISAM2 update -> resolve_ar -> result. cssrlib_gnss.py exposes load_rtk / load_ppp (per-epoch measurement bundles), dd_observations / undiff_observations (ready-to-splat factor arguments) and resolve_ar (LAMBDA on the ISAM2 float). Results unchanged: RTK fixed 2D=0.007 m (28 amb), PPP-RTK fixed 2D=0.022 m (22 amb). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… notebook Rename cssrlib_gnss.py -> gnss_frontend.py and remove the proper noun from the notebook narrative, module docstring and section headings, keeping the front-end library only where unavoidable (the pip-install URL and internal imports). Env var CSSRLIB_DATA -> GNSS_DATA. Results unchanged (RTK fixed 2D=0.007 m, PPP-RTK fixed 2D=0.022 m). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update the pip-install URL (notebook setup cell and gnss_frontend.py docstring) from the auto-named claude/* branch to the renamed gtsam-gnss-frontend branch of the front-end fork. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Feature/ppp undifferenced notebook
|
The notebook helps a lot in showing the RTK vs PPP-RTK story. One documentation clarification: I initially wondered whether the ambiguity state Could we make that explicit in the notebook and the carrier-factor docs? For example:
That would make the modeling boundary very clear for readers who are newer to carrier-phase GNSS. I'd also shy away from acronyms like |
…e_ar Address PR review: make explicit that the carrier-phase ambiguity N is estimated as a continuous float (double, in cycles) inside the GTSAM factor graph and that integer fixing is a separate LAMBDA step. - CarrierPhaseFactor.h: add @note to CarrierPhaseFactor and UndifferencedCarrierPhaseFactor docstrings stating N is a float and integer resolution happens outside the graph. - RtkAndPppExample.ipynb: spell out the float/integer boundary in the RTK and PPP "resolve the integers" cells. - gnss_frontend.py / notebook: rename resolve_ar -> resolve_integer_ambiguities (avoid the AR acronym; verbose and clear). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds undifferenced (PPP) GNSS factors. In PPP the receiver clock, tropospheric zenith wet delay and slant ionosphere do not cancel by differencing, so they are carried as state variables:
UndifferencedPseudorangeFactor(+Arm) — keys[pos/pose, clock, ZTD, slant-iono];error = geodist(sat, rcv) + c*(dt_u - dt_s) + m_w*ZTD + mu_f*I - measured.UndifferencedCarrierPhaseFactor(+Arm) — additionally carries the float ambiguity (+ lam*N); the ionosphere enters with the opposite sign (carrier advance).m_w(tropospheric wet mapping) andmu_f(first-order iono coefficient) are constants per factor; ZTD and slant iono are estimated nodes. TheArmvariants take a body-frame lever arm and optionalecef_T_nav, matching the existing factors. Analytical Jacobians, Python bindings, and unit tests with numerical-derivative checks (EXPECT_CORRECT_FACTOR_JACOBIANS) are included.Geometry is routed through
gnss::geodist(Sagnac-corrected), consistent with the double-difference factors. Note: ondevelopthe existingPseudorangeFactor/CarrierPhaseFactorstill use a plain Euclidean range; #2574 routes them throughgnss::geodist, after which all factors compute geometry consistently.