Orbis.GNSS.IonosphereFree (Orbis v0.9.0)

Copy Markdown View Source

The dual-frequency ionosphere-free linear combination of pseudoranges.

The first-order ionospheric group delay on a GNSS pseudorange is dispersive: to first order it scales as 1 / f^2, so a signal at carrier f is delayed by I(f) = K / f^2 for a slant-path constant K proportional to the total electron content (TEC). Measuring the same range on two carriers f1 and f2 therefore gives two observations that share the geometry but carry different ionospheric delays, and a fixed linear combination of the two cancels the 1 / f^2 term exactly:

PR_IF = (f1^2 * PR1 - f2^2 * PR2) / (f1^2 - f2^2)

Writing gamma = f1^2 / (f1^2 - f2^2) this is the affine combination

PR_IF = gamma * PR1 - (gamma - 1) * PR2

Substituting PR_i = R + K / f_i^2 (a true range R plus the first-order ionospheric delay on band i) the K terms cancel and PR_IF = R. A position solve fed these combined pseudoranges therefore needs no ionosphere model — call Orbis.GNSS.Positioning.solve/4 with ionosphere: false (the troposphere term, which is non-dispersive and does not cancel, still applies).

Frequency table

The standard carrier frequencies used here, in hertz:

SystemBandFrequency (MHz)
GPSL11575.42
GPSL21227.60
GalileoE11575.42
GalileoE5a1176.45
BeiDouB1I1561.098
BeiDouB3I1268.52

The default combination pair per system is GPS {:l1, :l2}, Galileo {:e1, :e5a}, BeiDou {:b1i, :b3i}.

Noise amplification

The combination is not free: because it is a weighted difference of two noisy observations, uncorrelated band noise of equal standard deviation sigma is amplified to sigma * sqrt(gamma^2 + (gamma - 1)^2). For GPS L1/L2 this factor is about 2.978; for Galileo E1/E5a about 2.588. See noise_amplification/2.

Non-goals

This module builds only the first-order ionosphere-free pseudorange combination. It deliberately does not implement carrier-phase combinations, the Melbourne-Wubbena / wide-lane / geometry-free combinations, ambiguity resolution, the second-order ionospheric term, or triple-frequency combinations.

Summary

Types

A reason a satellite was dropped from the paired set.

A pseudorange observation {satellite_id, range_m}.

Functions

The standard combination band pair for system.

The full carrier-frequency table as %{system => %{band => f_hz}}.

The carrier frequency in hertz for system ("G", "E", "C") and band.

The ionosphere-free combination coefficient gamma = f1^2 / (f1^2 - f2^2).

The ionosphere-free pseudorange from two carrier-band pseudoranges.

Convenience: pull two carrier bands from a parsed observation handle and combine them into ionosphere-free pseudoranges for one epoch.

Combine two per-satellite pseudorange bands into ionosphere-free pseudoranges.

The noise-amplification factor sqrt(gamma^2 + (gamma - 1)^2).

Types

drop_reason()

@type drop_reason() :: :missing_band1 | :missing_band2 | :unknown_system

A reason a satellite was dropped from the paired set.

observation()

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

A pseudorange observation {satellite_id, range_m}.

Functions

default_pair(system)

@spec default_pair(String.t()) ::
  {:ok, {atom(), atom()}} | {:error, {:unknown_system, String.t()}}

The standard combination band pair for system.

Returns {:ok, {band1, band2}} or {:error, {:unknown_system, system}}.

frequencies()

@spec frequencies() :: %{required(String.t()) => %{required(atom()) => float()}}

The full carrier-frequency table as %{system => %{band => f_hz}}.

frequency(system, band)

@spec frequency(String.t(), atom()) ::
  {:ok, float()} | {:error, {:unknown_band, String.t(), atom()}}

The carrier frequency in hertz for system ("G", "E", "C") and band.

Returns {:ok, f_hz} or {:error, {:unknown_band, system, band}}.

gamma(f1, f2)

@spec gamma(float(), float()) :: {:ok, float()} | {:error, :equal_frequencies}

The ionosphere-free combination coefficient gamma = f1^2 / (f1^2 - f2^2).

Returns {:error, :equal_frequencies} when f1 == f2 (the combination is undefined; the denominator vanishes). Never raises.

iono_free(pr1, pr2, f1, f2)

@spec iono_free(float(), float(), float(), float()) ::
  {:ok, float()} | {:error, :equal_frequencies}

The ionosphere-free pseudorange from two carrier-band pseudoranges.

PR_IF = (f1^2 * pr1 - f2^2 * pr2) / (f1^2 - f2^2)
      = gamma * pr1 - (gamma - 1) * pr2

pr1/pr2 are in metres on carriers f1/f2 (hertz). Returns {:ok, pr_if}, or {:error, :equal_frequencies} when f1 == f2. Never raises.

iono_free_from_obs(obs, epoch, opts \\ [])

@spec iono_free_from_obs(
  Orbis.GNSS.RINEX.Observations.t(),
  non_neg_integer() | tuple(),
  keyword()
) ::
  {:ok, {[observation()], [{String.t(), drop_reason()}]}} | {:error, term()}

Convenience: pull two carrier bands from a parsed observation handle and combine them into ionosphere-free pseudoranges for one epoch.

Calls Orbis.GNSS.RINEX.Observations.pseudoranges/3 twice — once for each band's code preference — then iono_free_pseudoranges/3. epoch is an epoch index or {{y, mo, d}, {h, mi, s}} tuple, exactly as Observations.pseudoranges/3 accepts.

Returns {:ok, {combined, dropped}} or {:error, reason} (propagated from either extraction).

Options

  • :codes — a map %{system => {band1_codes, band2_codes}} of the observation codes to extract for each band, e.g. %{"G" => {["C1C"], ["C2W", "C2L"]}, "E" => {["C1C"], ["C5Q"]}}. When omitted, the standard band-1/band-2 codes for the systems present in the file's observation_codes/1 are used (GPS C1C/C2W, Galileo C1C/C5Q, BeiDou C2I/C6I), restricted to those whose codes the file actually carries.
  • :pairs — forwarded to iono_free_pseudoranges/3.

iono_free_pseudoranges(band1, band2, opts \\ [])

@spec iono_free_pseudoranges([observation()], [observation()], keyword()) ::
  {[observation()], [{String.t(), drop_reason()}]}

Combine two per-satellite pseudorange bands into ionosphere-free pseudoranges.

band1 and band2 are [{satellite_id, range_m}] lists for the two carriers. Satellites are paired by id, and each pair is combined with the frequency pair for that satellite's system (the leading letter of the id), so a mixed GPS+Galileo+BeiDou set uses each system's own carriers.

Returns {combined, dropped} where combined is the ascending-by-id list of {satellite_id, pr_if} and dropped reports every satellite that could not be combined as {satellite_id, reason}:

  • :missing_band2 — present in band1 only;
  • :missing_band1 — present in band2 only;
  • :duplicate_observation — the satellite appears more than once in a band, so which pseudorange to use is ambiguous; it is dropped rather than silently collapsed to whichever entry comes last;
  • :unknown_system — the system letter has no known frequency pair.

Empty input yields {[], []}. Never raises.

Options

  • :pairs — override the band pair per system, e.g. pairs: %{"G" => {:l1, :l2}}. A system without an override uses its standard default pair.

noise_amplification(f1, f2)

@spec noise_amplification(float(), float()) ::
  {:ok, float()} | {:error, :equal_frequencies}

The noise-amplification factor sqrt(gamma^2 + (gamma - 1)^2).

This is the factor by which uncorrelated equal-variance band noise is amplified into the combined pseudorange. About 2.978 for GPS L1/L2 and 2.588 for Galileo E1/E5a. Returns {:error, :equal_frequencies} when f1 == f2. Never raises.