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.
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
@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.
A {satellite_id, elevation_deg} or {satellite_id, elevation_deg, cn0_dbhz} entry.
Functions
@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.
Computed via the Wilson-Hilferty approximation
x ~= k * (1 - 2/(9k) + z_p * sqrt(2/(9k)))^3where z_p is the standard-normal quantile for cumulative probability p
(obtained from the Acklam rational inverse-normal approximation). For
fault-detection use, p = 1 - p_fa.
In the deep upper tail at small k (the regime the fault test uses, e.g.
p = 0.999 for p_fa = 1e-3) the Wilson-Hilferty form is accurate to a few
percent and is a consistently conservative over-estimate of the true
critical value (which is the safe direction for fault detection: it errs
toward fewer false alarms). The published reference critical values at
p = 0.999, alongside what this function actually returns, are:
dof: 1 2 3 4 5
reference: 10.828 13.816 16.266 18.467 20.515 (chi-square tables)
chi2_inv: 11.157 14.133 16.551 18.724 20.751 (this function)i.e. an over-estimate of ~3.0% at dof = 1 shrinking to ~1.2% at dof = 5.
The result is clamped to be non-negative: a chi-square quantile is always
>= 0, but the cubed Wilson-Hilferty base can go negative for strongly
negative z_p (deep lower tail at small k), which the documented
fault-detection usage (p near 1) never reaches.
@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 (default1.0e-3):weights- forwarded toraim/2(default:unit):max_iterations- maximum number of exclusions to attempt (defaultlength(observations) - 4, never less than0)
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}).
@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 (default0.3):b- elevation-scaled term, metres (default0.3):model-:elevation(default) or:elevation_cn0:cn0- carrier-to-noise densityC/N0in 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.
@spec raim( Orbis.GNSS.Positioning.Solution.t(), keyword() ) :: raim_result()
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 (default1.0e-3); any other value raisesArgumentError:weights- either:unit(default; all sigma = 1) or a%{sat => weight}map of positive inverse-variance weights1 / sigma_i^2as built byweight_vector/2; an unspecified satellite defaults to unit weight. A non-positive or non-numeric weight raisesArgumentError: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.
@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.
@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.