Orbis.GNSS.Velocity (Orbis v0.9.1)

Copy Markdown View Source

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_drift

which 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_i

Stacking 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_unit is e_i, the receiver -> satellite ECEF unit vector;
  • range_rate_m_s for a static receiver is exactly e_i . v_sat_i (the los . (v_sat - v_rx) projection with v_rx = 0), already expressed in the consistent receive-epoch ECEF frame with light-time and Sagnac corrections applied. The satellite velocity is finite-differenced inside predict/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

observation()

@type observation() :: {String.t(), number()}

receiver()

@type receiver() :: vec3() | %{x_m: number(), y_m: number(), z_m: number()}

result()

@type result() :: %{
  velocity_m_s: vec3(),
  speed_m_s: float(),
  clock_drift_s_s: float(),
  residuals_m_s: %{required(String.t()) => float()},
  used_sats: [String.t()],
  n_satellites: non_neg_integer()
}

vec3()

@type vec3() :: {float(), float(), float()}

Functions

doppler_to_range_rate(doppler_hz, carrier_hz \\ Constants.gps_l1_hz())

@spec doppler_to_range_rate(number(), number()) :: float()

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.

range_rate_to_doppler(rho_dot_m_s, carrier_hz \\ Constants.gps_l1_hz())

@spec range_rate_to_doppler(number(), number()) :: float()

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.

solve(source, observations, epoch, receiver_position, opts \\ [])

@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 carrier 1575.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, default true.
  • :sagnac - apply the Sagnac / Earth-rotation correction, default true.

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.