Sidereon.GNSS.QC (Sidereon v3.0.0)

Copy Markdown View Source

Measurement-quality control for single-point positioning.

The numerical modeling and FDE orchestration live in the sidereon-core Rust core. This module keeps the Elixir API, normalizes options and epochs for the NIF, maps errors, and decodes the unchanged public result maps.

Summary

Types

FDE state returned when the core stops with a detected fault.

The public exception raised or returned for a core quality-control refusal.

The result of raim/2.

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

Functions

Chi-square inverse CDF (quantile).

Fault detection and exclusion: solve, run RAIM, compare leave-one-out candidate solves when a fault is detected, and repeat until the accepted solution passes RAIM or the exclusion budget is exhausted.

Lint RINEX NAV text.

Lint a parsed RINEX OBS file.

Lint RINEX OBS or CRINEX text.

Observation completeness and signal-quality rollup for a parsed RINEX OBS file.

Pseudorange measurement variance (m^2) from satellite elevation.

Residual-based RAIM: a chi-square goodness-of-fit test on a positioning solution.

Standalone range RAIM/FDE over a caller-supplied linearized measurement set, independent of any full positioning solve.

Residual-based RAIM for an existing SPP solution.

Render an observation QC report as HTML.

Render an observation QC report as fixed-width text.

Mechanically repair RINEX NAV text.

Mechanically repair RINEX OBS or CRINEX text.

Core robust-reweighted SPP under the RAIM/FDE exclusion loop.

Build a satellite => sigma_m map for a list of weight entries.

Serialize an observation QC report as JSON.

Build a satellite => inverse_variance_weight map for a list of weight entries.

Types

fde_unresolved()

@type fde_unresolved() :: %{
  reason: String.t(),
  solution: Sidereon.GNSS.Positioning.Solution.t(),
  excluded: [{String.t(), :raim_excluded}],
  iterations: non_neg_integer(),
  raim: raim_result()
}

FDE state returned when the core stops with a detected fault.

quality_error()

@type quality_error() :: Sidereon.GNSS.QC.QualityError.t()

The public exception raised or returned for a core quality-control refusal.

raim_result()

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

The result of raim/2.

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(number(), integer()) :: float()

Chi-square inverse CDF (quantile).

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

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

Fault detection and exclusion: solve, run RAIM, compare leave-one-out candidate solves when a fault is detected, and repeat until the accepted solution passes RAIM or the exclusion budget is exhausted.

:pseudorange_code selects the code of the pseudoranges as in Sidereon.GNSS.Positioning.solve/4 (default :single_frequency).

Malformed FDE options are returned as tagged errors, including {:invalid_option, :p_fa}, {:invalid_option, :weights}, {:invalid_option, :max_exclusions}, {:invalid_option, :max_exclusion_rms_m} and {:invalid_option, :pseudorange_code}. Weighting defaults to the solution's actual per-satellite variances; weights: :unit and a positive satellite map remain explicit alternatives. max_exclusions defaults to one and the RMS cap defaults to 100 metres; use :infinity to remove the cap. The removed :max_iterations option is explicitly rejected, including when supplied together with :max_exclusions. Core quality refusals return {:error, %Sidereon.GNSS.QC.QualityError{}}; malformed binding options remain tagged with :invalid_option. Success returns the accepted :solution, ordered :excluded list, number of exclusions, and the accepted solution's :raim result. An unresolved fault returns {:error, {:fault_unresolved, unresolved}}; the map includes the core :reason ("exclusion_budget_exhausted" or "no_admissible_exclusion"), last :solution, :excluded list, :iterations, and its faulted :raim result. An ephemeris source that refuses a satellite state reading UT1 outside the UT1 table fails with {:ut1_outside_coverage, :before_coverage | :after_coverage}.

lint_nav_text(text)

@spec lint_nav_text(String.t()) :: {:ok, map()} | {:error, term()}

Lint RINEX NAV text.

A NavImplausibleRecord detail keeps finite :value fields as floats; non-finite values are represented by :nan, :infinity, or :negative_infinity.

lint_obs(observations)

@spec lint_obs(Sidereon.GNSS.RINEX.Observations.t()) ::
  {:ok, map()} | {:error, term()}

Lint a parsed RINEX OBS file.

Each finding preserves :code, :severity, :spec_ref, :repairable, and :at, and also reports :kind plus :details. A populated detail is a tagged tuple such as {:obs_event_epoch, %{flag: 4}}; a fieldless detail is the variant atom, such as :obs_interval_unavailable.

lint_obs_text(text)

@spec lint_obs_text(String.t()) :: {:ok, map()} | {:error, term()}

Lint RINEX OBS or CRINEX text.

Findings include their tagged variant and typed payload in :details, while retaining the existing code, severity, spec reference, repairability, and source-location fields.

observation_report(observations, opts \\ [])

@spec observation_report(
  Sidereon.GNSS.RINEX.Observations.t(),
  keyword()
) :: {:ok, Sidereon.GNSS.QC.ObservationReport.t()} | {:error, term()}

Observation completeness and signal-quality rollup for a parsed RINEX OBS file.

pseudorange_variance(elevation_deg, opts \\ [])

@spec pseudorange_variance(
  number(),
  keyword()
) :: float() | {:error, Sidereon.GNSS.QC.QualityError.kind()}

Pseudorange measurement variance (m^2) from satellite elevation.

Returns a float or a tagged core refusal. The core accepts finite elevations in [-90, 90]; a zero-elevation observation is valid when b: 0.0, while a nonzero elevation-scaled term has undefined variance at the horizon.

raim(input, opts \\ [])

Residual-based RAIM: a chi-square goodness-of-fit test on a positioning solution.

The default weights: :solution uses the actual per-satellite variances from the solution or RaimInput; pass weights: :unit or a satellite-weight map for the other modes. Solution input also carries its actual receiver-clock count so shared GPS/QZSS/SBAS clocks are not counted as separate parameters.

Pass inverse-variance weights derived from per-satellite residual variances, either as a %{sat => weight} map or as weight entries consumed by weight_vector/2. Unit weights with metre-scale residuals make fault_detected saturate near 100%.

A core refusal raises Sidereon.GNSS.QC.QualityError, whose :kind is the core variant (for example :missing_variances or :invalid_variance). Binding-level malformed options continue to raise ArgumentError.

entries = [
  %{satellite_id: "G01", elevation_deg: 72.0},
  %{satellite_id: "G02", elevation_deg: 42.0}
]

weights = Sidereon.GNSS.QC.weight_vector(entries, a_m: 0.8, b_m: 0.8)
Sidereon.GNSS.QC.raim(input, weights: weights)

raim_fde_design(rows, opts \\ [])

@spec raim_fde_design(
  [map()],
  keyword()
) :: {:ok, map()} | {:error, term()}

Standalone range RAIM/FDE over a caller-supplied linearized measurement set, independent of any full positioning solve.

Each row of rows is a map describing one linearized range measurement:

  • :id - stable measurement identifier (e.g. a satellite token "G01")
  • :residual_m - observed-minus-computed range residual, metres
  • :design_row - the measurement's row of the design matrix (a list of the partials of the predicted range with respect to each estimated state parameter); every row must carry the same length
  • :weight - inverse-variance weight 1 / sigma^2, strictly positive

Options:

  • :p_fa - false-alarm probability for the global chi-square test (default 0.001)
  • :max_exclusions - maximum measurements the exclusion loop may remove (default: one, matching RTKLIB demo5)
  • :min_redundancy - minimum redundancy an exclusion must leave behind (default 1)
  • :max_exclusion_rms_m - largest admissible unweighted residual RMS for a leave-one-out candidate (default 100.0; :infinity disables this cap)

Returns {:ok, result} where result carries the protected :state_correction, :state_covariance, the :global_test chi-square map, the :excluded ids, per-measurement :diagnostics, and the exclusion :iterations; or {:error, reason} for a malformed or rank-deficient input. The removed :max_iterations option is explicitly rejected as {:invalid_option, :max_iterations}. Core quality refusals return {:error, %Sidereon.GNSS.QC.QualityError{}}.

raim_for_solution(solution, opts \\ [])

@spec raim_for_solution(
  Sidereon.GNSS.Positioning.Solution.t(),
  keyword()
) :: raim_result()

Residual-based RAIM for an existing SPP solution.

This is the direct post-solve variant matching the Rust and C raim_for_solution surface. It uses the solution's used satellites and post-fit residuals, actual variances and solved receiver-clock count, with the same options accepted by raim/2. An explicit :n_systems overrides that count.

render_html(observation_report)

@spec render_html(Sidereon.GNSS.QC.ObservationReport.t()) ::
  {:ok, String.t()} | {:error, term()}

Render an observation QC report as HTML.

render_text(observation_report)

@spec render_text(Sidereon.GNSS.QC.ObservationReport.t()) ::
  {:ok, String.t()} | {:error, term()}

Render an observation QC report as fixed-width text.

repair_nav_text(text, opts \\ [])

@spec repair_nav_text(
  String.t(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Mechanically repair RINEX NAV text.

Content-changing repairs other than record sorting are opt-in. The repaired product's iono map carries every broadcast ionosphere set the header states: gps, beidou, qzss and navic Klobuchar {alpha, beta} pairs, galileo NeQuick G coefficients, galileo_disturbance_flags and the BeiDou BDGIM beidou_bdgim coefficients, each nil when absent.

Returns {:error, {:repaired_product_unwritable, {:not_representable, line, reason}}} when the repaired record set cannot be written as RINEX navigation text, such as a set holding both a CNAV-family record and an unclassified Galileo record.

repair_obs_text(text, opts \\ [])

@spec repair_obs_text(
  String.t(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Mechanically repair RINEX OBS or CRINEX text.

Content-changing repairs other than record sorting are opt-in. Set :set_interval to true to replace an unavailable or mismatched INTERVAL from the observed epoch cadence, for example.

The repaired product is written with the observation writer, which returns text only when it reads back as the product, and that text is then compressed to CRINEX. On success the map holds both texts, the actions taken and the lint report after repair. Each failure keeps its own shape:

  • {:error, {:parse, message}} or {:error, {:invalid_input, message}} - the input does not read, or repair refuses it, such as text whose unretained header records a repair would drop without drop_unsupported: true.
  • {:error, {:repaired_product_unwritable, {tag, fields}}} - the repaired product cannot be written exactly; {tag, fields} is one of the writer refusals Sidereon.GNSS.RINEX.Observations lists.
  • {:error, {:crinex_encode_failed, {kind, message}}} - the written RINEX does not compress, kind being :parse, :invalid_input or :unhandled.

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

@spec robust_fde(
  term(),
  [Sidereon.GNSS.Positioning.observation()],
  Sidereon.GNSS.Positioning.epoch(),
  keyword()
) ::
  {:ok,
   %{
     solution: Sidereon.GNSS.Positioning.Solution.t(),
     excluded: [{String.t(), :raim_excluded}],
     iterations: non_neg_integer(),
     raim: raim_result()
   }}
  | {:error, {:fault_unresolved, fde_unresolved()}}
  | {:error, term()}

Core robust-reweighted SPP under the RAIM/FDE exclusion loop.

The successful result carries the accepted solution, ordered exclusions, exclusion count and that solution's :raim test. An unresolved fault carries the core stop reason, last solution, exclusions and faulted RAIM result.

sigmas(entries, opts \\ [])

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

Build a satellite => sigma_m map for a list of weight entries.

to_json(observation_report)

@spec to_json(Sidereon.GNSS.QC.ObservationReport.t()) ::
  {:ok, String.t()} | {:error, term()}

Serialize an observation QC report as JSON.

weight_vector(entries, opts \\ [])

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

Build a satellite => inverse_variance_weight map for a list of weight entries.