# `Sidereon.GNSS.QC`
[🔗](https://github.com/neilberkman/sidereon-ex/blob/main/lib/sidereon/gnss/qc.ex#L1)

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.

# `raim_result`

```elixir
@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()) =&gt; float()},
  rms_m: float(),
  worst_sat: String.t() | nil
}
```

The result of `raim/2`.

# `weight_entry`

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

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

# `chi2_inv`

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

Chi-square inverse CDF (quantile).

# `fde`

```elixir
@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()
   }}
  | {:error, {:fault_unresolved, float()}}
  | {:error, term()}
```

Fault detection and exclusion: solve, run RAIM, exclude the worst satellite,
and repeat until the measurement set is self-consistent or the exclusion
budget is exhausted.

Malformed FDE options are returned as tagged errors, including
`{:invalid_option, :p_fa}`, `{:invalid_option, :weights}`, and
`{:invalid_option, :max_iterations}`.

# `lint_nav_text`

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

Lint RINEX NAV text.

# `lint_obs`

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

Lint a parsed RINEX OBS file.

# `lint_obs_text`

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

Lint RINEX OBS or CRINEX text.

# `observation_report`

```elixir
@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`

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

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

Returns a float, `{:error, :invalid_elevation}` for elevations at or below the
horizon, or `{:error, :missing_cn0}` when `model: :elevation_cn0` is selected
without `:cn0`.

# `raim`

```elixir
@spec raim(
  Sidereon.GNSS.Positioning.Solution.t() | Sidereon.GNSS.QC.RaimInput.t(),
  keyword()
) :: raim_result() | Sidereon.GNSS.QC.RaimResult.t()
```

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

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%.

    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`

```elixir
@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: the row count)
  * `:min_redundancy` - minimum redundancy an exclusion must leave behind
    (default `1`)

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.

# `raim_for_solution`

```elixir
@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, with the same options accepted by `raim/2`.

# `render_html`

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

Render an observation QC report as HTML.

# `render_text`

```elixir
@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`

```elixir
@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.

# `repair_obs_text`

```elixir
@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.

# `robust_fde`

```elixir
@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()
   }}
  | {:error, term()}
```

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

# `sigmas`

```elixir
@spec sigmas(
  [weight_entry()],
  keyword()
) :: %{required(String.t()) =&gt; float()}
```

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

# `to_json`

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

Serialize an observation QC report as JSON.

# `weight_vector`

```elixir
@spec weight_vector(
  [weight_entry()],
  keyword()
) :: %{required(String.t()) =&gt; float()}
```

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
