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

A unified satellite-ephemeris sampling surface over precise SP3 products and
broadcast navigation messages.

`sample/3` evaluates a list of satellites across an epoch range at a fixed step
and returns a tidy per-satellite, per-epoch table of ECEF position and satellite
clock bias. The call is **identical** whether the source is an `Sidereon.GNSS.SP3`
precise product or an `Sidereon.GNSS.Broadcast` navigation product, so a caller can
swap precise for broadcast transparently; the dispatch is on the handle's type.

The grid sampler delegates to the core `ephemeris::sample` API. The underlying
products are parsed once into resource handles and reused across every cell; no
file is re-read per epoch.

The broadcast models follow IS-GPS-200 (GPS LNAV), the Galileo OS-SIS-ICD, and
the BeiDou BDS-SIS-ICD; precise products are SP3-c / SP3-d (IGS).

## Frame, time, and sign conventions

  * **Frame:** position is ITRF/IGS-realization ECEF, in meters
    (`x_m`, `y_m`, `z_m`), the same frame both products evaluate in.
  * **Time:** every epoch is interpreted in **GPS time (GPST)**. No
    leap-second shifting is applied to the supplied epochs; the broadcast
    evaluator maps GPST onto each system's own scale (BDT for BeiDou,
    UTC-referenced for GLONASS) internally.
  * **Clock sign:** `clock_s` is the satellite clock offset in seconds, with a
    **positive value meaning the satellite clock is ahead of system time**. The
    pseudorange geometric correction is therefore `range + c * clock_s`. The SP3
    and broadcast paths share this convention (the broadcast value is the
    clock-polynomial total including the relativistic eccentricity term and the
    broadcast group delay).

## Gaps are explicit

A cell whose satellite has no valid ephemeris at its epoch carries
`status: :no_ephemeris` and `nil` position/clock fields. The sampler **never**
extrapolates beyond a product's validity: an SP3 epoch outside the file's span,
or a broadcast epoch outside any record's fit interval, is reported as a gap,
not filled.

## Example

    {:ok, sp3} = Sidereon.GNSS.SP3.load("igs.sp3")

    rows =
      Sidereon.GNSS.Ephemeris.sample(sp3, ["G01", "E11"], %{
        from: ~N[2020-06-25 00:00:00],
        to: ~N[2020-06-25 01:00:00],
        step_s: 300
      })

    [%Sidereon.GNSS.Ephemeris.Row{} = row | _] = rows
    row.satellite_id   # "G01"
    row.status         # :ok | :no_ephemeris
    row.x_m            # ITRF/IGS ECEF X, meters (nil on a gap)

# `source`

```elixir
@type source() :: Sidereon.GNSS.SP3.t() | Sidereon.GNSS.Broadcast.t()
```

A parsed ephemeris source: a precise SP3 product or a broadcast product.

# `window`

```elixir
@type window() :: %{
  from: NaiveDateTime.t(),
  to: NaiveDateTime.t(),
  step_s: pos_integer()
}
```

The sampling window: a `from`/`to` epoch pair (inclusive of `from`, and of `to`
when it falls on a step boundary) and a positive `step_s` in seconds.

# `sample`

```elixir
@spec sample(source(), [String.t()], window()) :: [Sidereon.GNSS.Ephemeris.Row.t()]
```

Sample `sat_ids` across the `window` from a precise or broadcast source.

`source` is a loaded `Sidereon.GNSS.SP3` or `Sidereon.GNSS.Broadcast` handle,
with the same call shape for both. `sat_ids` is a list of canonical RINEX tokens
(`"G01"`, `"E11"`, `"C06"`, `"R07"`). `window` is a map with `:from`, `:to`
(`NaiveDateTime` in GPST), and `:step_s` (a positive integer number of seconds).

Returns a flat list of `Sidereon.GNSS.Ephemeris.Row` structs, one per
satellite-epoch cell, in `sat_ids` order then ascending epoch. A cell with no
valid ephemeris carries `status: :no_ephemeris` and `nil` values; the handle is
reused for every cell, so no file is re-read.

Raises `ArgumentError` for a non-positive step or a window with `to` before
`from`.

---

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