Skip to content

navigation: add GNSS Doppler (range-rate) factors#2589

Open
inuex35 wants to merge 7 commits into
borglab:developfrom
inuex35:feature/doppler-factor
Open

navigation: add GNSS Doppler (range-rate) factors#2589
inuex35 wants to merge 7 commits into
borglab:developfrom
inuex35:feature/doppler-factor

Conversation

@inuex35

@inuex35 inuex35 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Adds GNSS Doppler (range-rate) factors, complementing PseudorangeFactor /
CarrierPhaseFactor.

  • DopplerFactor — keys [velocity, clock bias(k-1), clock bias(k)]:

    error = e·(v_s - v_r)
          + c·((dt_r(k) - dt_r(k-1))/dt - ddt_s)
          + sagnac_rate
          - (-λ·Doppler)
    
  • DopplerFactorArm — keys [pose, velocity, clock bias(k-1), clock bias(k)]:
    lever-arm variant using v_antenna = v_body + ecef_R_body·(omega × leverArm),
    with an optional ecef_T_nav overload; reduces to DopplerFactor when omega = 0.

Both include the earth-rotation (Sagnac) rate correction and reuse the
Sagnac-corrected line-of-sight from gnss::geodist. Analytical Jacobians
(checked with EXPECT_CORRECT_FACTOR_JACOBIANS), Python bindings, and
obj/XML/binary serialization tests are included.

inuex35 and others added 7 commits July 2, 2026 11:31
DopplerFactor relates a measured Doppler to receiver velocity and clock drift,
reusing the gnss::geodist line-of-sight:
  error = e.(v_s - v_r) + c*(ddt_r - ddt_s) - (-lambda*Doppler)
Keys: [velocity, clock drift]. The satellite velocity and LOS are held constant
per factor (their dependence on receiver position is second order).

ClockDriftFactor encodes the constant-velocity (constant-drift) clock model,
linking two consecutive clock biases through the shared drift:
  error = bias(k) - bias(k-1) - drift*dt
This is the scalar analog of the IMU position/velocity link (not a plain
BetweenFactor, since the transition is non-identity). It lets Doppler-observed
drift propagate into the clock bias used by the pseudorange/carrier factors.

Adds Python bindings and a test (residual + numerical Jacobians for both).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The range-rate model omitted the time-derivative of the Sagnac range
correction that RTKLIB's resdop() includes (a ~cm/s effect). Add it,
split into a v_r-independent offset and a linear coefficient on the
receiver velocity, both precomputed in the constructor from the
satellite position/velocity and receiver position already passed in.
No change to gnss::geodist (it handles the position/range Sagnac; the
rate term lives in the velocity domain).

- evaluateError: predicted rate += sagnacOffset_ + velSagnac_.v_r;
  velocity Jacobian becomes (velSagnac_ - e)^T.
- Update print/equals/serialize for the two new members.
- testDopplerFactor: fold the Sagnac rate into the reference value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add DopplerFactorArm, the lever-arm variant of DopplerFactor, and address
code-review findings.

DopplerFactorArm keys on [Pose3, velocity, clock drift]. Unlike the
pseudorange/carrier Arm factors (which correct antenna *position*), the
dominant lever-arm effect on a Doppler observation is kinematic: a rotating
body moves the antenna, so the range rate uses
  v_antenna = v_body + ecef_R_body * (omega x leverArm)
with omega the measured body angular rate. The LOS/Sagnac terms use the
nominal receiver position (as in DopplerFactor), so the pose enters only
through attitude; with omega = 0 it reduces to DopplerFactor. An ecef_T_nav
overload supports a local nav-frame pose. Analytical Jacobians verified
against numerical derivatives (~1e-8) on the linked library.

Review fixes:
- Serialization: BOOST_SERIALIZATION_BASE_OBJECT_NVP used the qualified
  `DopplerFactor::Base` / `ClockDriftFactor::Base`, producing an invalid XML
  tag ("::") that made equalsXML throw. Use `Base` like the sibling factors.
  Regression-caught by the new serialization tests below.
- Add serialization round-trip tests (obj/XML/binary) for DopplerFactor,
  DopplerFactorArm (both overloads) and ClockDriftFactor.
- DopplerFactor::print() now also prints velSagnac_.
- Add DopplerFactorArm tests: reduction to base (omega=0), full model vs an
  independent reference, ecef_T_nav Jacobians, and equals; plus the Python
  binding in navigation.i.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ClockDriftFactor

Rework the GNSS Doppler factors to express the receiver clock drift as
(bias_k - bias_{k-1})/dt using the two adjacent clock-bias states already
estimated by the pseudorange/carrier factors, instead of a dedicated
clock-drift state:

- DopplerFactor keys change from [velocity, clockDrift] to
  [velocity, clockBiasPrev, clockBiasCurr], with a new dt parameter
- DopplerFactorArm gains the same two-bias keying (4 keys total)
- ClockDriftFactor is removed: the Doppler measurement itself now
  constrains the clock-bias evolution, so no extra between-epoch clock
  factor or drift state is needed

This keeps the state vector identical to the pseudorange-only problem
(TDCP-like structure).  Validated against the u-blox F9P sample data from
rtklibexplorer/rtklib-py (GPS L1, 30 epoch pairs): the joint two-epoch
solution matches the RTKLIB resdop-style estimation with an explicit
drift state to <0.3 mm/s in velocity and ~5e-13 s/s in drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUiPXeNrWr3cmpb2Lsa3HK
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUiPXeNrWr3cmpb2Lsa3HK
Doc/comment accuracy only (no behavior change):
- Drop the inaccurate "time derivative of the Sagnac range term" wording; the
  implemented sagnac_rate is simply the earth-rotation correction to the range
  rate (the literal derivative of the geodist Sagnac term has the opposite
  sign, so the old phrasing was misleading).
- Reword the base DopplerFactor note: the receiver position is a fixed input,
  not a state, so the line-of-sight is constant and enters no Jacobian (the
  old "second-order dependence ... is neglected" wording implied a state it
  does not have).
- Document the new keying's usage: the two clock-bias keys must be distinct
  adjacent-epoch states (same key -> zero drift), the first epoch has no k-1
  bias, and per-constellation biases share the common drift.
- DopplerFactorArm::print() now also prints velSagnac_ and, when set, the
  ecef_T_nav transform (matching equals()/serialize()).
- Add a DopplerFactorArm dt<=0 guard test.
- Refresh the @Date.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ck-drift-review-srk4qi

# Conflicts:
#	gtsam/navigation/tests/testSerializationNavigation.cpp
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants