Code-differential GNSS (DGPS) positioning over single-frequency pseudoranges.
A base receiver at a surveyed (known) position turns its raw pseudoranges
into per-satellite pseudorange corrections (PRC). A rover applies those
corrections to its own pseudoranges and runs a point-positioning solve. The
subtraction pr_rover - PRC forms a single difference between the two
receivers that cancels every error common to both — satellite-clock error,
ephemeris error, and (over a short baseline) the ionospheric and tropospheric
delays — leaving the rover position observable. This is the classical
code-differential / RTCM-style PRC technique.
Measurement model
A measured single-frequency pseudorange is
pr = geometric_range + c*rx_clock - c*sat_clock + atmosphere + ephemeris_error + noiseAt the known base the modelled value, written to match exactly the terms the
point-positioning estimator removes internally (geometric_range - c*sat_clock),
is
m_base(sat) = geometric_range(base, sat) - c*sat_clock(sat)The pseudorange correction is the standard DGPS PRC
PRC(sat) = pr_base(sat) - m_base(sat)Expanding pr_base shows the geometric range and the satellite-clock term
cancel by construction, so
PRC(sat) = c*rx_clock_base + atmosphere_base(sat) + ephemeris_error(sat) + noise_base(sat)i.e. everything common to both receivers plus the base receiver clock, which is a single per-station constant shared by every satellite.
The rover forms
pr_rover_corrected(sat) = pr_rover(sat) - PRC(sat)and runs Orbis.GNSS.Positioning.solve/4 with ionosphere: false and
troposphere: false (the differential already removed those delays). The
estimator re-applies its own geometric_range(rover, sat) - c*sat_clock + c*rx_clock_rover model. Because m_base subtracted -c*sat_clock exactly
once and pr_rover still carries -c*sat_clock once, the corrected
pseudorange contains the satellite-clock term exactly once and the estimator
removes it exactly once: the satellite clock is never double-counted.
Single-difference cancellation
For a per-satellite additive error e(sat) common to base and rover (a
satellite-clock error, an ephemeris error, or a short-baseline atmospheric
delay): pr_base gains +e(sat), so PRC(sat) gains +e(sat); then
pr_rover_corrected = (pr_rover + e) - (PRC + e) cancels e(sat) to machine
precision.
Base receiver clock
PRC contributes the constant c*rx_clock_base to every satellite, so after
pr_rover - PRC each rover pseudorange carries the same satellite-independent
offset. A constant common to all pseudoranges is indistinguishable from a
receiver-clock bias, so the estimator's recovered rover clock simply absorbs
rx_clock_rover - rx_clock_base; the rover position is unaffected.
Frame consistency
Orbis.GNSS.Observables.predict/5 is evaluated with light_time: true and
sagnac: true so the base modelled range lives in the same transmit-time,
Earth-rotation-corrected frame the estimator uses; this is what makes the PRC
consistent with the estimator's internal model.
Non-goals
This module covers single-frequency code-differential positioning only.
Carrier-phase double differences, RTK / integer-ambiguity resolution, RTCM
wire-format message encoding/decoding, network/VRS corrections, and a moving
base are out of scope. A range-rate correction (RRC) is also a non-goal: the
static single-epoch design carries no base/rover time offset over which to
propagate it, even though Orbis.GNSS.Observables exposes range_rate_m_s.
Summary
Types
Per-satellite pseudorange corrections in meters.
A {satellite_id, pseudorange_m} pseudorange observation.
An ECEF position as {x_m, y_m, z_m} or %{x_m, y_m, z_m}.
A satellite id string, e.g. "G01".
Functions
Apply corrections to rover observations, pairing by satellite.
Compute per-satellite pseudorange corrections (PRC) from the surveyed base.
Differential position solve for the rover from base + rover pseudoranges.
Types
Per-satellite pseudorange corrections in meters.
A {satellite_id, pseudorange_m} pseudorange observation.
An ECEF position as {x_m, y_m, z_m} or %{x_m, y_m, z_m}.
@type sat() :: String.t()
A satellite id string, e.g. "G01".
Functions
@spec apply([observation()], corrections()) :: {[observation()], [sat()]}
Apply corrections to rover observations, pairing by satellite.
Returns {corrected, dropped} where corrected is the list of
{satellite_id, pr_corrected_m} for every satellite present in both the
rover observations and the corrections (pr_corrected = pr_rover - PRC(sat)),
and dropped is the list of rover satellite ids that have no correction.
Corrections without a matching rover observation are ignored. The pairing is
order-independent (it is keyed on the satellite id).
This shadows Kernel.apply/2 inside the module; call it qualified as
Orbis.GNSS.DGNSS.apply/2.
@spec corrections( Orbis.GNSS.SP3.t(), position(), [observation()], NaiveDateTime.t(), keyword() ) :: {:ok, corrections()} | {:error, term()}
Compute per-satellite pseudorange corrections (PRC) from the surveyed base.
source is a loaded Orbis.GNSS.SP3 product; base_position is the known base
ECEF position; base_observations is a list of {satellite_id, pseudorange_m} pairs; epoch is the receive epoch (NaiveDateTime, in the
product's time scale).
For each base observation the modelled value
m_base = geometric_range(base, sat) - c*sat_clock(sat) is taken from
Orbis.GNSS.Observables.predict/5 (light-time and Sagnac on) and the
correction is PRC = pr_base - m_base. A satellite whose ephemeris cannot be
evaluated at this epoch is dropped from the result (it cannot be corrected)
rather than failing the batch.
Returns {:ok, %{sat => prc_m}}, or a tagged error —
{:error, :invalid_base_position} for a malformed base position,
{:error, :empty_base_observations}, or
{:error, {:invalid_base_observations, term}} for a bad shape. Never raises.
@spec position( Orbis.GNSS.SP3.t(), position(), [observation()], [observation()], NaiveDateTime.t(), keyword() ) :: {:ok, map()} | {:error, term()}
Differential position solve for the rover from base + rover pseudoranges.
Computes the base corrections, applies them to the rover observations, and
runs Orbis.GNSS.Positioning.solve/4 on the corrected pseudoranges with
ionosphere: false and troposphere: false forced on (the differential has
already removed those delays — caller-supplied values for those two options
are overridden). The :initial_guess option and any meteorology/Klobuchar
options are passed through, though the atmosphere terms are disabled.
On success returns
{:ok, %{
solution: %Orbis.GNSS.Positioning.Solution{},
baseline_vector_m: %{x_m: float(), y_m: float(), z_m: float()},
baseline_m: float(),
dropped_sats: [sat()]
}}where the baseline vector points from the base position to the solved rover
position and baseline_m is its length. Errors from any stage — a bad
observation shape or a point-positioning error such as
{:too_few_satellites, used, required} — are propagated as {:error, reason}. Never raises.