Orbis.GNSS.Observables (Orbis v0.9.1)

Copy Markdown View Source

Predict the GNSS observables a receiver at a known ECEF position would see for a satellite, from a precise (SP3) ephemeris source.

This is the forward model behind the question "is this measurement physically plausible?": given a receiver position, a satellite, and a receive epoch, it computes the geometric range, the line-of-sight range rate, the L1 Doppler, the topocentric azimuth/elevation, the satellite clock offset, and the signal transmit time. It reads satellite states only from Orbis.GNSS.SP3.position/3 and uses standard textbook GNSS geometry; it never solves the inverse (positioning) problem.

Algorithm (standard GNSS geometry)

  • Light-time / transmit-time correction. The signal seen at the receive epoch t_rx left the satellite earlier, at t_tx = t_rx - |r_sat(t_tx) - r_rx| / c. This is solved by fixed-point iteration starting from t_tx = t_rx; a couple of iterations converge to sub-millimetre level for a coarse receiver position. The satellite state is evaluated at the fractional epoch t_tx (the SP3 spline is sampled at sub-second precision).

  • Sagnac / Earth-rotation correction. During the travel time tau the Earth-fixed (ECEF) frame rotates by omega_e * tau. The satellite position computed in the ECEF frame at t_tx is rotated about the Z axis by Rz(omega_e * tau) into the receive-epoch ECEF frame before differencing, with omega_e = 7.2921151467e-5 rad/s. This is the Sagnac (Earth-rotation) correction.

  • Geometric range is |r_sat_rot - r_rx| in metres, and the line-of-sight unit vector points from the receiver to the satellite.

  • Range rate. The satellite velocity at t_tx is obtained by central finite difference of Orbis.GNSS.SP3.position/3 (+/- 0.5 s). For a static receiver (v_rx = 0) the range rate is the LOS projection los . (v_sat - v_rx), which equals d(range)/dt.

  • Doppler (IS-GPS-200 L1 carrier). doppler_hz = -range_rate * f / c with the L1 carrier f = 1575.42 MHz and c = 299792458 m/s.

Sign conventions

range_rate_m_s is the time derivative of the geometric range: it is negative when the satellite is approaching (range decreasing) and positive when receding. The Doppler shift is the negative of the (scaled) range rate, so an approaching satellite gives a positive Doppler and a receding satellite a negative one.

Result map

%{
  geometric_range_m: float(),    # metres
  range_rate_m_s:    float(),    # d(range)/dt; negative = approaching
  doppler_hz:        float(),    # = -range_rate * carrier / c; + = approaching
  sat_clock_s:       float() | nil,  # SP3 clock offset at transmit time
  elevation_deg:     float(),    # topocentric elevation
  azimuth_deg:       float(),    # topocentric azimuth, [0, 360)
  transmit_time:     NaiveDateTime.t(),  # t_tx
  los_unit:          {float(), float(), float()}  # receiver -> satellite, ECEF unit
}

Summary

Functions

Predict the observables for satellite_id seen from receiver_ecef at epoch.

Predict observables for every satellite in the product, seen from receiver_ecef.

Types

observables()

@type observables() :: %{
  geometric_range_m: float(),
  range_rate_m_s: float(),
  doppler_hz: float(),
  sat_clock_s: float() | nil,
  elevation_deg: float(),
  azimuth_deg: float(),
  transmit_time: NaiveDateTime.t(),
  los_unit: vec3()
}

vec3()

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

Functions

predict(sp3, satellite_id, receiver_ecef, epoch, opts \\ [])

@spec predict(
  Orbis.GNSS.SP3.t(),
  String.t(),
  vec3() | map(),
  NaiveDateTime.t(),
  keyword()
) ::
  {:ok, observables()} | {:error, term()}

Predict the observables for satellite_id seen from receiver_ecef at epoch.

receiver_ecef is the static receiver position in ITRF/ECEF metres, given as {x_m, y_m, z_m} or %{x_m: _, y_m: _, z_m: _}. epoch is the receive epoch, a NaiveDateTime (interpreted in the SP3 file's own time scale).

Options

  • :carrier_hz - carrier frequency for the Doppler, default the L1 carrier 1575.42 MHz.
  • :light_time - apply the light-time / transmit-time correction, default true. When false, the satellite is evaluated at epoch.
  • :sagnac - apply the Sagnac / Earth-rotation correction, default true.

Returns {:ok, observables}, {:error, :invalid_receiver} for a malformed receiver position, or propagates any Orbis.GNSS.SP3.position/3 error (e.g. an unknown satellite or a malformed satellite token) verbatim as {:error, reason}. Never raises.

Note that Orbis.GNSS.SP3.position/3 extrapolates its spline rather than reporting a coverage error, so an epoch outside the file's span does not yield a tagged error here; it produces an obviously non-physical geometry instead.

predict_all(sp3, receiver_ecef, epoch, opts \\ [])

@spec predict_all(Orbis.GNSS.SP3.t(), vec3() | map(), NaiveDateTime.t(), keyword()) ::
  %{
    optional(String.t()) => {:ok, observables()} | {:error, term()}
  }

Predict observables for every satellite in the product, seen from receiver_ecef.

Returns a map satellite_id => {:ok, observables} | {:error, reason}, so one satellite failing (e.g. no estimate at this epoch) does not sink the batch. Options are the same as predict/5.