Orbis.GNSS.DGNSS (Orbis v0.9.1)

Copy Markdown View Source

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 + noise

At 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

corrections()

@type corrections() :: %{optional(sat()) => float()}

Per-satellite pseudorange corrections in meters.

observation()

@type observation() :: {sat(), number()}

A {satellite_id, pseudorange_m} pseudorange observation.

position()

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

An ECEF position as {x_m, y_m, z_m} or %{x_m, y_m, z_m}.

sat()

@type sat() :: String.t()

A satellite id string, e.g. "G01".

Functions

apply(rover_observations, corrections)

@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.

corrections(source, base_position, base_observations, epoch, opts \\ [])

@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.

position(source, base_position, base_observations, rover_observations, epoch, opts \\ [])

@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.