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

Single-frequency ionospheric group-delay corrections.

Two models are exposed over the `sidereon-core` crate: the GPS broadcast
Klobuchar model (eight alpha/beta coefficients, IS-GPS-200), and an IONEX
vertical-TEC-grid slant delay (single-layer model). Both return the group
delay in **positive meters**.

This is the ionosphere correction for a GNSS signal. It is **not**
`Sidereon.Atmosphere`, which is NRLMSISE-00 neutral-atmosphere mass density for
drag, a different quantity entirely.

## Sign convention

The returned delay is a **group delay** and is **positive**: it increases the
measured pseudorange (the signal arrives later than vacuum geometry would
predict). The carrier-phase advance is the negation of this value. The
ionosphere is dispersive, so the delay reported on a carrier other than the
model's native L1 is the L1 delay scaled by `(f_L1 / f)^2`; pass the carrier
via `frequency_hz`.

## Units at the boundary

Public inputs are in degrees (`_deg`) and meters/hertz, per the Sidereon naming
convention. Latitude is positive north, longitude positive east, azimuth
clockwise from north.

# `from_node_samples`

```elixir
@spec from_node_samples(
  [Sidereon.GNSS.Ionosphere.TecSample.t()],
  number(),
  number(),
  integer()
) ::
  {:ok, reference()} | {:error, term()}
```

Build an IONEX product from one sample per TEC grid node.

`shell_height_km` and `base_radius_km` are kilometers. Each
`Sidereon.GNSS.Ionosphere.TecSample` carries latitude/longitude in degrees
and vertical TEC/RMS in TECU.

# `from_samples`

```elixir
@spec from_samples(Sidereon.GNSS.Ionosphere.TecGridSamples.t()) ::
  {:ok, reference()} | {:error, term()}
```

Build an IONEX product directly from whole-grid TEC samples.

Axes are degrees, shell and base radii are kilometers, and TEC/RMS grids are
TECU. The returned handle is accepted anywhere a parsed IONEX handle is
accepted, including `ionex_slant_delay/7`.

# `galileo_nequick_g_delay`

```elixir
@spec galileo_nequick_g_delay(
  map(),
  number(),
  number(),
  number(),
  number(),
  NaiveDateTime.t() | tuple(),
  number()
) :: {:ok, float()} | {:error, term()}
```

Galileo NeQuick-G single-frequency ionospheric group delay, scaled to
`frequency_hz`.

`coeffs` carries the three Galileo broadcast effective-ionisation coefficients
as `%{ai0: a0, ai1: a1, ai2: a2}`. The receiver geodetic latitude/longitude and
the satellite azimuth/elevation are in degrees; `epoch` is a `NaiveDateTime` or
`{{y, m, d}, {h, min, s}}` tuple in Galileo system time. The NeQuick-G arm maps
the slant delay by elevation only, so `azimuth_deg` is accepted (to match the
shared ionosphere-model boundary) but does not change the result. Returns
`{:ok, delay_m}` (positive meters) or `{:error, reason}`.

# `ionex_slant_delay`

```elixir
@spec ionex_slant_delay(
  reference(),
  number(),
  number(),
  number(),
  number(),
  NaiveDateTime.t() | tuple(),
  number()
) :: {:ok, float()} | {:error, term()}
```

IONEX vertical-TEC-grid slant ionospheric group delay, scaled to `frequency_hz`.

`handle` is a parsed-IONEX reference from `parse_ionex/1` or `load_ionex/1`.
The receiver geodetic latitude/longitude and the satellite azimuth/elevation
are in degrees; `epoch` is a `NaiveDateTime` or `{{y, m, d}, {h, min, s}}`
tuple (the pierce point rides on the IONEX shell, so the receiver height is not
used). Returns `{:ok, delay_m}` (positive meters) or `{:error, reason}`.

# `ionex_to_string`

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

Serialize a parsed IONEX product back to standard IONEX text.

`handle` is a parsed-IONEX reference from `parse_ionex/1` or `load_ionex/1`.
This is the inverse of `parse_ionex/1`: re-parsing the output reproduces the
same TEC grids. The serialization is deterministic and performs no I/O.

Returns `{:ok, text}` or `{:error, reason}`.

## Examples

    {:ok, handle} = Sidereon.GNSS.Ionosphere.parse_ionex(ionex_bytes)
    {:ok, text} = Sidereon.GNSS.Ionosphere.ionex_to_string(handle)
    {:ok, _reparsed} = Sidereon.GNSS.Ionosphere.parse_ionex(text)

# `klobuchar_delay`

```elixir
@spec klobuchar_delay(
  map(),
  number(),
  number(),
  number(),
  number(),
  NaiveDateTime.t() | tuple(),
  number()
) :: {:ok, float()} | {:error, term()}
```

GPS broadcast Klobuchar L1 ionospheric group delay, scaled to `frequency_hz`.

`params` carries the eight broadcast coefficients as
`%{alpha: {a0, a1, a2, a3}, beta: {b0, b1, b2, b3}}` (or lists). The receiver
geodetic latitude/longitude and the satellite azimuth/elevation are in
degrees; `epoch` is a `NaiveDateTime` or `{{y, m, d}, {h, min, s}}` tuple in
GPS time. Returns `{:ok, delay_m}` (positive meters) or `{:error, reason}`.

# `load_ionex`

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

Load and parse an IONEX file into a product handle.

Returns `{:ok, reference()}` or `{:error, reason}`.

# `nequick_g_delay`

```elixir
@spec nequick_g_delay(map(), map(), number()) :: {:ok, float()} | {:error, term()}
```

Galileo NeQuick-G full slant ionospheric group delay (positive meters).

The full 3D slant TEC from `nequick_g_stec/2` mapped to a dispersive group
delay on `frequency_hz`. `coeffs` and `ray` are as in `nequick_g_stec/2`.

Returns `{:ok, delay_m}` (positive meters) or `{:error, reason}`.

# `nequick_g_stec`

```elixir
@spec nequick_g_stec(map(), map()) :: {:ok, float()} | {:error, term()}
```

Galileo NeQuick-G full three-dimensional slant total electron content (TECU).

This is the reference-grade full 3D NeQuick-G integration, distinct from the
compact single-layer `galileo_nequick_g_delay/7`. The `coeffs` map carries the
broadcast effective-ionisation coefficients `%{ai0:, ai1:, ai2:}`. The `ray`
map carries both endpoints of the receiver-to-satellite line of sight, in the
reference algorithm's native units:

  * `:month` - month of the year, `1..12`
  * `:utc_hours` - UTC time of day in hours, `[0, 24]`
  * `:station_lon_deg`, `:station_lat_deg`, `:station_height_m`
  * `:satellite_lon_deg`, `:satellite_lat_deg`, `:satellite_height_m`

Returns `{:ok, stec_tecu}` (slant TEC in TECU) or `{:error, reason}`.

# `parse_ionex`

```elixir
@spec parse_ionex(binary()) :: {:ok, reference()} | {:error, term()}
```

Parse an in-memory IONEX byte buffer into a product handle.

Returns `{:ok, reference()}` or `{:error, reason}`. The buffer is parsed
exactly once; the parsed grid is held as a resource handle.

# `tec_grid_samples`

```elixir
@spec tec_grid_samples(reference()) ::
  Sidereon.GNSS.Ionosphere.TecGridSamples.t() | {:error, term()}
```

Extract an IONEX handle as whole-grid TEC samples.

This is the inverse IR for `from_samples/1`. Axes are degrees, shell and base
radii are kilometers, and TEC/RMS grids are TECU.

# `tec_samples`

```elixir
@spec tec_samples(reference()) ::
  [Sidereon.GNSS.Ionosphere.TecSample.t()] | {:error, term()}
```

Extract an IONEX handle as one TEC sample per grid node.

Each returned sample has latitude/longitude in degrees and vertical TEC/RMS in
TECU.

---

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