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

Recover receiver velocity and clock drift from one epoch of range-rate or
Doppler observations against a precise SP3 or broadcast ephemeris source.

The numerical model and least-squares solve live in the Rust GNSS core. This
module preserves the Elixir API shape: input normalization, per-satellite
option resolution, and public result/error maps.

# `observation`

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

One range-rate or Doppler observation.

# `receiver`

```elixir
@type receiver() :: vec3() | %{x_m: number(), y_m: number(), z_m: number()}
```

Receiver ECEF position in metres.

# `result`

```elixir
@type result() :: %{
  velocity_m_s: vec3(),
  speed_m_s: float(),
  clock_drift_s_s: float(),
  state_covariance: [[float()]],
  residuals_m_s: %{required(String.t()) =&gt; float()},
  used_sats: [String.t()],
  n_satellites: non_neg_integer()
}
```

Receiver velocity solve result with unit-variance state covariance.

# `vec3`

```elixir
@type vec3() :: {float(), float(), float()}
```

Three-component ECEF vector.

# `doppler_to_range_rate`

```elixir
@spec doppler_to_range_rate(number(), number()) :: float()
```

Convert a Doppler shift in Hz to a pseudorange rate in m/s.

# `range_rate_to_doppler`

```elixir
@spec range_rate_to_doppler(number(), number()) :: float()
```

Convert a pseudorange rate in m/s to a Doppler shift in Hz.

# `solve`

```elixir
@spec solve(
  Sidereon.GNSS.SP3.t() | Sidereon.GNSS.Broadcast.t(),
  [observation()],
  NaiveDateTime.t(),
  receiver(),
  keyword()
) :: {:ok, result()} | {:error, term()}
```

Solve for receiver velocity and clock drift at one receive epoch.

`observations` are `{satellite_id, value}` pairs. Values are pseudorange rates
in m/s by default, or Doppler shifts in Hz with `observable: :doppler`.

---

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