Orbis.GNSS.QC (Orbis v0.26.0)

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, {:fault_unresolved, float()}}
  | {: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), except :huber: FDE is itself the leave-one-out robust path, so combining it with the crate-layer Huber/IRLS reweighting is undefined and huber: true returns {:error, {:incompatible_options, [:robust, :huber]}} (the same refusal solve/4 gives for robust: true, huber: true). 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.

When the exclusion loop runs out of permitted iterations (or out of satellites to exclude) while RAIM still flags the fix as faulted, the set could not be made self-consistent: rather than hand back a fix RAIM rejects, this returns {:error, {:fault_unresolved, test_statistic}}, carrying the final RAIM test statistic. A success exit ({:ok, _}) means either the set passed RAIM or the geometry has no redundancy left to test (dof <= 0), never a still-faulted fix at the iteration cap.

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.