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_rxleft the satellite earlier, att_tx = t_rx - |r_sat(t_tx) - r_rx| / c. This is solved by fixed-point iteration starting fromt_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 epocht_tx(the SP3 spline is sampled at sub-second precision).Sagnac / Earth-rotation correction. During the travel time
tauthe Earth-fixed (ECEF) frame rotates byomega_e * tau. The satellite position computed in the ECEF frame att_txis rotated about the Z axis byRz(omega_e * tau)into the receive-epoch ECEF frame before differencing, withomega_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_txis obtained by central finite difference ofOrbis.GNSS.SP3.position/3(+/- 0.5 s). For a static receiver (v_rx = 0) the range rate is the LOS projectionlos . (v_sat - v_rx), which equalsd(range)/dt.Doppler (IS-GPS-200 L1 carrier).
doppler_hz = -range_rate * f / cwith the L1 carrierf = 1575.42 MHzandc = 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
Functions
@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 carrier1575.42 MHz.:light_time- apply the light-time / transmit-time correction, defaulttrue. Whenfalse, the satellite is evaluated atepoch.:sagnac- apply the Sagnac / Earth-rotation correction, defaulttrue.
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.
@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.