Orbis.GNSS.QC (Orbis v0.9.1)

Copy Markdown View Source

Measurement-quality control for single-point positioning: a generic "is this measurement set self-consistent?" layer over Orbis.GNSS.Positioning.

Three standard receiver-autonomous integrity tools are provided, all from the textbook GNSS integrity literature:

  • Elevation- (and optionally C/N0-) dependent measurement weighting. A low-elevation or low-carrier-to-noise observation carries more noise, so it should be down-weighted in the solve and in the fault test. The model here is the standard RTKLIB-style elevation form (see pseudorange_variance/2).

  • Residual-based RAIM (receiver autonomous integrity monitoring). A chi-square goodness-of-fit test on the post-fit pseudorange residuals: the (optionally weighted) sum of squared residuals is compared against the chi-square critical value for the redundancy of the geometry. A statistic above the threshold flags an inconsistent measurement set (a likely fault). See raim/2.

  • Fault detection and exclusion (FDE). The standard leave-one-out exclusion loop: when RAIM detects a fault, the satellite with the largest normalized residual is removed and the position is re-solved, repeating until the set is self-consistent or too few satellites remain. See fde/4.

All math is standard practice; no positioning math is duplicated here — the re-solves go back through Orbis.GNSS.Positioning.solve/4.

Degrees of freedom

The position estimate has three position coordinates plus one receiver clock per distinct GNSS system (a mixed-constellation set carries one clock column per constellation). The number of estimated states is therefore n_states = 3 + n_systems, and the redundancy available to the fault test is

dof = n_used - n_states = n_used - (3 + n_systems)

where n_systems is the count of distinct system letters (the leading character of each satellite id, e.g. "G", "E") in the used satellites. With dof <= 0 the geometry has no redundancy and no fault test is possible.

Summary

Types

The result of raim/2.

A {satellite_id, elevation_deg} or {satellite_id, elevation_deg, cn0_dbhz} entry.

Functions

Chi-square inverse CDF (quantile): the value x such that P(X <= x) = p for a chi-square distribution with k degrees of freedom. p must be strictly between 0 and 1, and k must be a positive integer.

Fault detection and exclusion: solve, run RAIM, and if a fault is detected, exclude the worst satellite and re-solve, repeating until the measurement set is self-consistent or too few satellites remain.

Pseudorange measurement variance (m^2) from satellite elevation, using the standard elevation-dependent weighting model.

Residual-based RAIM: a chi-square goodness-of-fit test on a solution's post-fit pseudorange residuals.

Build a satellite => sigma_m map (the per-observation standard deviation in metres) for a list of weight entries.

Build a satellite => weight map, where each weight is 1 / sigma^2, the inverse-variance weight used by a weighted least-squares solve and by the weighted RAIM test.

Types

raim_result()

@type raim_result() :: %{
  fault_detected?: boolean(),
  test_statistic: float(),
  threshold: float() | nil,
  dof: integer(),
  testable?: boolean(),
  normalized_residuals: %{required(String.t()) => float()},
  worst_sat: String.t() | nil
}

The result of raim/2.

fault_detected? is true when the test statistic exceeds the chi-square threshold. test_statistic is the (optionally weighted) sum of squared residuals; threshold is the chi-square critical value at the requested false-alarm probability for dof degrees of freedom (or nil when the geometry is not testable). testable? is false exactly when dof <= 0. normalized_residuals maps each used satellite to its standardized residual r_i / sigma_i, and worst_sat is the satellite with the largest-magnitude normalized residual (the leave-one-out exclusion candidate), or nil when there are no residuals.

weight_entry()

@type weight_entry() :: {String.t(), number()} | {String.t(), number(), number()}

A {satellite_id, elevation_deg} or {satellite_id, elevation_deg, cn0_dbhz} entry.

Functions

chi2_inv(p, k)

@spec chi2_inv(float(), pos_integer()) :: float()

Chi-square inverse CDF (quantile): the value x such that P(X <= x) = p for a chi-square distribution with k degrees of freedom. p must be strictly between 0 and 1, and k must be a positive integer.

The chi-square CDF is the regularized lower incomplete gamma function P(k/2, x/2). This function inverts that CDF by bracketing and bisection, evaluating P(a, x) with the standard series / continued-fraction split from Numerical Recipes. It is dependency-free but checked against scipy's scipy.stats.chi2.ppf oracle in the test fixture.

fde(source, observations, epoch, opts \\ [])

@spec fde(
  term(),
  [Orbis.GNSS.Positioning.observation()],
  Orbis.GNSS.Positioning.epoch(),
  keyword()
) ::
  {:ok,
   %{
     solution: Orbis.GNSS.Positioning.Solution.t(),
     excluded: [{String.t(), :raim_excluded}],
     iterations: non_neg_integer()
   }}
  | {:error, term()}

Fault detection and exclusion: solve, run RAIM, and if a fault is detected, exclude the worst satellite and re-solve, repeating until the measurement set is self-consistent or too few satellites remain.

This is the standard leave-one-out FDE loop. At each step the satellite with the largest normalized residual (worst_sat from raim/2) is dropped from the observation list and Orbis.GNSS.Positioning.solve/4 is re-run on the remainder. The loop stops when RAIM passes, when there is no redundancy left to test (dof <= 0), or when the iteration bound is reached.

Options

All Orbis.GNSS.Positioning.solve/4 options are forwarded to every re-solve (e.g. :initial_guess, :ionosphere, :troposphere). Additionally:

  • :p_fa - false-alarm probability for the RAIM test (default 1.0e-3)
  • :weights - forwarded to raim/2 (default :unit)
  • :max_iterations - maximum number of exclusions to attempt (default length(observations) - 4, never less than 0)

Returns {:ok, %{solution: Solution.t(), excluded: [{sat, :raim_excluded}], iterations: non_neg_integer()}}excluded is the list of removed satellites in exclusion order (empty for a clean set), and iterations is the number of exclusion steps performed. Returns {:error, reason} if any solve fails (the solve/4 reason is propagated, e.g. {:too_few_satellites, used, required}).

pseudorange_variance(elevation_deg, opts \\ [])

@spec pseudorange_variance(
  number(),
  keyword()
) :: float() | {:error, :invalid_elevation | :missing_cn0}

Pseudorange measurement variance (m^2) from satellite elevation, using the standard elevation-dependent weighting model.

The chosen form is the RTKLIB-style variance model

sigma^2(el) = a^2 + b^2 / sin^2(el)

with el the topocentric elevation. As el -> 90 deg the variance tends to a^2 + b^2 (the zenith floor); as el -> 0 it grows without bound, so a low-elevation observation is heavily down-weighted. The model is therefore monotonically decreasing in elevation: the lowest satellite always has the largest variance.

Options

  • :a - zenith term, metres (default 0.3)
  • :b - elevation-scaled term, metres (default 0.3)
  • :model - :elevation (default) or :elevation_cn0
  • :cn0 - carrier-to-noise density C/N0 in dB-Hz, required for :elevation_cn0

With model: :elevation_cn0 a standard carrier-to-noise term is added to the elevation variance:

sigma^2 = a^2 + b^2 / sin^2(el) + c0 * 10^(-cn0/10)

A higher C/N0 (a stronger signal) yields a smaller added variance, matching standard C/N0-based measurement weighting. c0 is a reference scale (:cn0_scale, default 1.0 m^2).

Returns the variance as a float, or {:error, :invalid_elevation} when elevation_deg <= 0 (a satellite at or below the horizon has no usable weighting), and {:error, :missing_cn0} when the C/N0 model is selected without a :cn0 value.

raim(solution, opts \\ [])

Residual-based RAIM: a chi-square goodness-of-fit test on a solution's post-fit pseudorange residuals.

Given an Orbis.GNSS.Positioning.Solution, this computes the (optionally weighted) sum of squared residuals

T = sum_i r_i^2 / sigma_i^2        (weighted)
T = sum_i r_i^2                    (unit weights, the default)

which under the no-fault hypothesis is distributed chi-square with dof degrees of freedom (see the moduledoc for dof = n_used - (3 + n_systems)). A fault is declared when T exceeds the chi-square critical value chi2_inv(1 - p_fa, dof) for the requested false-alarm probability.

The per-satellite normalized residual r_i / sigma_i is the standardized residual; worst_sat is the satellite with the largest-magnitude normalized residual, i.e. the standard largest-normalized-residual exclusion candidate.

Options

  • :p_fa - false-alarm probability, a number strictly between 0 and 1 (default 1.0e-3); any other value raises ArgumentError
  • :weights - either :unit (default; all sigma = 1) or a %{sat => weight} map of positive inverse-variance weights 1 / sigma_i^2 as built by weight_vector/2; an unspecified satellite defaults to unit weight. A non-positive or non-numeric weight raises ArgumentError
  • :n_systems - override the distinct-system count; by default it is derived from the leading character of each used satellite id

When dof <= 0 the geometry carries no redundancy: the result reports fault_detected?: false, testable?: false, threshold: nil, and the computed dof without raising.

sigmas(entries, opts \\ [])

@spec sigmas(
  [weight_entry()],
  keyword()
) :: %{required(String.t()) => float()}

Build a satellite => sigma_m map (the per-observation standard deviation in metres) for a list of weight entries.

Each entry is {sat, elevation_deg} or, for the C/N0 model, {sat, elevation_deg, cn0_dbhz}. Options are forwarded to pseudorange_variance/2. An entry with an invalid elevation is dropped from the result.

weight_vector(entries, opts \\ [])

@spec weight_vector(
  [weight_entry()],
  keyword()
) :: %{required(String.t()) => float()}

Build a satellite => weight map, where each weight is 1 / sigma^2, the inverse-variance weight used by a weighted least-squares solve and by the weighted RAIM test.

Entries and options are as for sigmas/2. An entry with an invalid elevation is dropped.