Recover a receiver's velocity and clock drift from one epoch of Doppler or pseudorange-rate measurements against a precise (SP3) ephemeris source.
This is the velocity counterpart to Orbis.GNSS.Positioning: where single-point
positioning inverts pseudoranges for position and clock bias, this module inverts
pseudorange rates (or equivalently Doppler shifts) for the receiver velocity and
clock drift, given a known receiver position. It reuses the forward model in
Orbis.GNSS.Observables for the satellite geometry and velocity, and the explicit
4x4 inverse in Orbis.GNSS.Geometry.inv4/1 for the normal-equation solution.
Observation model (standard Doppler / range-rate least squares)
For satellite i with line-of-sight unit vector e_i (receiver -> satellite,
ECEF), satellite velocity v_sat_i, receiver velocity v_rx, the measured
pseudorange rate is
rho_dot_i = e_i . (v_sat_i - v_rx) + c * (rx_clock_drift - sat_clock_drift_i)Collecting the four unknowns into x = [v_rx_x, v_rx_y, v_rx_z, c * rx_clock_drift]
(the clock term carried in length-rate units, m/s), the model rearranges to a
linear system
e_i . v_sat_i - rho_dot_i - c * sat_clock_drift_i = e_i . v_rx - c * rx_clock_driftwhich is one row H_i . x = y_i with
H_i = [-e_ix, -e_iy, -e_iz, 1]
y_i = rho_dot_i - (e_i . v_sat_i) + c * sat_clock_drift_iStacking the visible satellites and solving the (unweighted) normal equations
gives x = (H^T H)^-1 H^T y, from which the receiver velocity is {x0, x1, x2}
and the clock drift is x3 / c (seconds per second).
How the geometry terms are obtained
Every quantity comes from Orbis.GNSS.Observables.predict/5 evaluated at the
known receiver position, which is treated as static for the forward model:
los_unitise_i, the receiver -> satellite ECEF unit vector;range_rate_m_sfor a static receiver is exactlye_i . v_sat_i(thelos . (v_sat - v_rx)projection withv_rx = 0), already expressed in the consistent receive-epoch ECEF frame with light-time and Sagnac corrections applied. The satellite velocity is finite-differenced insidepredict/5, so it is never accessed directly here.
Satellite clock drift
The per-satellite term c * sat_clock_drift_i is a known correction folded into
y_i. By default it is taken as zero: the SP3 forward model exposes the
satellite clock offset but not its time derivative, and a constant common
satellite-clock-drift bias is absorbed by the estimated receiver clock drift,
exactly as a common clock offset is absorbed by the receiver clock bias in
single-point positioning. Callers who have per-satellite drift estimates may
supply them through opts[:sat_clock_drift] (a %{sat => drift_s_s} map or a
one-argument function of the satellite id); the same value is then subtracted
from y_i.
Doppler observations
A Doppler shift relates to the pseudorange rate by doppler_hz = -rho_dot * f / c
(the convention in Orbis.GNSS.Observables), so with opts[:observable] = :doppler
each measurement is converted via rho_dot = -doppler_hz * c / f before the solve,
with the carrier f taken from opts[:carrier_hz] (default L1, 1575.42 MHz).
Result map
%{
velocity_m_s: {vx, vy, vz}, # receiver velocity, ECEF m/s
speed_m_s: float(), # norm of velocity_m_s
clock_drift_s_s: float(), # receiver clock drift, s/s
residuals_m_s: %{sat => r}, # post-fit range-rate residuals, m/s
used_sats: [sat], # satellites contributing, build order
n_satellites: non_neg_integer()
}
Summary
Functions
Convert a Doppler shift in Hz to a pseudorange rate in m/s.
Convert a pseudorange rate in m/s to a Doppler shift in Hz.
Solve for the receiver velocity and clock drift at one receive epoch.
Types
Functions
Convert a Doppler shift in Hz to a pseudorange rate in m/s.
Uses rho_dot = -doppler_hz * c / carrier_hz, the inverse of the Doppler
convention in Orbis.GNSS.Observables (doppler_hz = -rho_dot * carrier_hz / c).
carrier_hz defaults to the L1 carrier.
Convert a pseudorange rate in m/s to a Doppler shift in Hz.
Uses doppler_hz = -rho_dot * carrier_hz / c, matching Orbis.GNSS.Observables.
carrier_hz defaults to the L1 carrier.
@spec solve( Orbis.GNSS.SP3.t(), [observation()], NaiveDateTime.t(), receiver(), keyword() ) :: {:ok, result()} | {:error, term()}
Solve for the receiver velocity and clock drift at one receive epoch.
source is a loaded Orbis.GNSS.SP3 precise product. observations is a list of
{satellite_id, value} pairs: pseudorange rates in m/s by default, or Doppler
shifts in Hz when opts[:observable] is :doppler. epoch is the receive
epoch (a NaiveDateTime in the product's time scale). receiver_position is
the known receiver ECEF position in metres, as {x_m, y_m, z_m} or
%{x_m: _, y_m: _, z_m: _} (typically a prior position solve).
Options
:observable-:range_rate(default) or:doppler.:carrier_hz- carrier frequency for the Doppler conversion, default the L1 carrier1575.42 MHz.:sat_clock_drift- per-satellite clock drift in s/s, as a%{sat => drift}map or a one-argument function of the satellite id; default zero for every satellite (absorbed into the receiver clock drift; see the module docs).:light_time- apply the light-time correction in the forward model, defaulttrue.:sagnac- apply the Sagnac / Earth-rotation correction, defaulttrue.
A satellite whose forward-model prediction fails (e.g. no ephemeris at this epoch) is dropped from the used set rather than failing the whole solve.
Returns {:ok, result} or {:error, reason}, where reason is one of
:no_observations (empty list), {:too_few_satellites, used, 4} (fewer than
four usable satellites), :singular_geometry (rank-deficient normal matrix),
:invalid_receiver (malformed receiver), {:invalid_observation, entry}
(malformed observation entry), or {:duplicate_observation, sat} (a satellite
appears more than once). Never raises.