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

RTK-facing carrier/code double-difference primitives.

A base receiver and a rover receiver observing the same satellites have
receiver-clock terms that differ by station but are common to every satellite.
A *single difference* subtracts base from rover for the same satellite; a
*double difference* subtracts a reference satellite's single difference:

    DD_s = (rover_s - base_s) - (rover_ref - base_ref)

The receiver clocks cancel in the second subtraction. Satellite-clock,
ephemeris, and short-baseline atmosphere errors that are common between base
and rover also cancel in the receiver single difference. The remaining
carrier-phase double differences are the measurement surface used by RTK
baseline estimation and integer ambiguity fixing.

`double_differences/3` returns normalized measurements. `solve_rtk_float/1`
and `solve_rtk_fixed/1` operate on already prepared RTK epochs.

## Example

    iex> base = [
    ...>   {"G01", 20_100.0, 20_103.0},
    ...>   {"G02", 21_105.0, 21_110.0}
    ...> ]
    iex> rover = [
    ...>   {"G01", 20_040.0, 20_044.0},
    ...>   {"G02", 21_060.0, 21_066.0}
    ...> ]
    iex> {:ok, result} = Sidereon.GNSS.RTK.double_differences(base, rover, reference_satellite_id: "G01")
    iex> result.double_differences
    [%{satellite_id: "G02", reference_satellite_id: "G01", ambiguity_id: "G02", code_m: 15.0, phase_m: 17.0}]

# `baseline_epoch`

```elixir
@type baseline_epoch() :: %{
  :base_observations =&gt; [observation()],
  :rover_observations =&gt; [observation()],
  :satellite_positions_m =&gt; satellite_positions(),
  optional(:base_satellite_positions_m) =&gt; satellite_positions(),
  optional(:rover_satellite_positions_m) =&gt; satellite_positions(),
  optional(:velocity_mps) =&gt; ecef_input(),
  optional(:epoch) =&gt; term()
}
```

One RTK epoch carrying paired base/rover observations and satellite positions.

`:epoch` is preserved in residual diagnostics; it is not interpreted by this
first solver layer because satellite positions are supplied by the caller.
`:satellite_positions_m` is used for satellite selection and elevation
weighting. When the caller has receiver-specific transmit-time positions, it
may also provide `:base_satellite_positions_m` and
`:rover_satellite_positions_m`; otherwise both default to
`:satellite_positions_m`.

# `double_difference`

```elixir
@type double_difference() :: %{
  satellite_id: String.t(),
  reference_satellite_id: String.t(),
  ambiguity_id: String.t(),
  code_m: float(),
  phase_m: float()
}
```

One non-reference satellite's double-difference observation.

# `dual_frequency_baseline_epoch`

```elixir
@type dual_frequency_baseline_epoch() :: %{
  :base_observations =&gt; [dual_frequency_observation()],
  :rover_observations =&gt; [dual_frequency_observation()],
  :satellite_positions_m =&gt; satellite_positions(),
  optional(:base_satellite_positions_m) =&gt; satellite_positions(),
  optional(:rover_satellite_positions_m) =&gt; satellite_positions(),
  optional(:epoch) =&gt; term()
}
```

One RTK epoch carrying raw dual-frequency base/rover observations.

# `dual_frequency_observation`

```elixir
@type dual_frequency_observation() :: %{
  :satellite_id =&gt; String.t(),
  :p1_m =&gt; number(),
  :p2_m =&gt; number(),
  :phi1_cyc =&gt; number(),
  :phi2_cyc =&gt; number(),
  :f1_hz =&gt; number(),
  :f2_hz =&gt; number(),
  optional(:ambiguity_id) =&gt; String.t(),
  optional(:lli1) =&gt; integer() | nil,
  optional(:lli2) =&gt; integer() | nil
}
```

Raw dual-frequency code/carrier observation for wide-lane/narrow-lane RTK.

`p1_m` / `p2_m` are code pseudoranges in metres, `phi1_cyc` / `phi2_cyc` are
carrier phases in cycles, and `f1_hz` / `f2_hz` are the corresponding carrier
frequencies. `:ambiguity_id` is normally omitted; the wide-lane solver sets it
internally when `:on_cycle_slip` is `:split_arc`.

# `ecef_input`

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

ECEF position in metres.

# `observation`

```elixir
@type observation() ::
  %{
    :satellite_id =&gt; String.t(),
    :code_m =&gt; number(),
    :phase_m =&gt; number(),
    optional(:ambiguity_id) =&gt; String.t(),
    optional(:lli) =&gt; integer() | nil,
    optional(:loss_of_lock_indicator) =&gt; integer() | nil
  }
  | {String.t(), number(), number()}
```

Code and carrier-phase observation in metres.

Map observations may optionally carry `:ambiguity_id` to identify a carrier
arc and `:lli` (or `:loss_of_lock_indicator`) for single-frequency
loss-of-lock handling. Tuple observations use the satellite id as the
ambiguity id and have no LLI.

# `result`

```elixir
@type result() :: %{
  reference_satellite_id: String.t(),
  double_differences: [double_difference()],
  dropped_sats: [String.t()]
}
```

Double-difference result with deterministic satellite ordering.

# `satellite_positions`

```elixir
@type satellite_positions() :: %{required(String.t()) =&gt; ecef_input()}
```

Satellite ECEF position keyed by satellite id.

# `double_differences`

```elixir
@spec double_differences([observation()], [observation()], keyword()) ::
  {:ok, result()} | {:error, term()}
```

Build code and carrier-phase double differences from base and rover observations.

Observations can be maps with `:satellite_id`, `:code_m`, and `:phase_m`, or
`{satellite_id, code_m, phase_m}` tuples. Satellites are paired by id; any
satellite not present at both receivers is reported in `:dropped_sats`.

Options:

  * `:reference_satellite_id` - reference satellite for the second
    difference: a satellite id binary (single-system data only) or a
    per-system map covering every observed system. When omitted, each
    system's lexicographically first common satellite is selected
    deterministically. Non-reference satellites difference against their own
    system's reference.

Returns `{:ok, result}` or a tagged error. At least two common satellites are
required so one non-reference double difference can be produced.

# `fix_wide_lane_rtk_arc`

```elixir
@spec fix_wide_lane_rtk_arc(
  [dual_frequency_baseline_epoch()],
  Sidereon.GNSS.RTK.WideLaneArcConfig.t() | map()
) :: {:ok, Sidereon.GNSS.RTK.WideLaneArcSolution.t()} | {:error, term()}
```

Fix wide-lane RTK arc ambiguities by delegating to the core arc helper.

The returned struct includes `:geometry_quality` for the wide-lane ambiguity
design.

# `prepare_ionosphere_free_rtk_arc`

```elixir
@spec prepare_ionosphere_free_rtk_arc(
  [dual_frequency_baseline_epoch()],
  %{required(String.t()) =&gt; integer()},
  Sidereon.GNSS.RTK.IonosphereFreeArcConfig.t() | map()
) :: {:ok, Sidereon.GNSS.RTK.IonosphereFreeArcSolution.t()} | {:error, term()}
```

Build ionosphere-free RTK arc epochs from dual-frequency epochs and fixed wide-lane integers.

# `solve_arc`

```elixir
@spec solve_arc([map()], Sidereon.GNSS.RTK.ArcConfig.t() | map()) ::
  {:ok, Sidereon.GNSS.RTK.ArcSolution.t()} | {:error, term()}
```

Solve a sequential RTK baseline arc from raw rover+base epochs, delegating the
whole driver (epoch normalization, reference selection, sequential filter, and
per-epoch ambiguity search) to the `sidereon-core` `solve_rtk_arc` kernel.

This is a thin delegation to the core arc driver. `epochs` is a list of raw
epoch maps:

  * `:base`, `:rover` - lists of observation maps
    `%{satellite_id:, ambiguity_id:, code_m:, phase_m:}`
  * `:satellite_positions_m` - `%{satellite_id => {x, y, z}}` shared-position map
  * `:base_satellite_positions_m`, `:rover_satellite_positions_m` - optional
    per-receiver transmit-time position maps (default to the shared map)
  * `:velocity_mps` - optional rover ECEF velocity `{vx, vy, vz}`
  * `:prediction_time_s` - optional epoch time coordinate

`config` is a map:

  * `:base_m` - base station ECEF `{x, y, z}`
  * `:reference` - `:auto` (default), `{:satellite, id}`, or
    `{:per_system, %{letter => id}}`
  * `:model` - `%{code_sigma_m:, phase_sigma_m:, stochastic_model:,
    elevation_weighting?:, sagnac?:}`
  * `:baseline_prior_sigma_m`, `:ambiguity_prior_sigma_m`
  * `:initial_baseline_m` - `{x, y, z}` (default `{0, 0, 0}`)
  * `:wavelengths_m`, `:offsets_m` - `%{ambiguity_id => value}`
  * `:update_opts` - the per-epoch update controls (see `arc_update_opts`)

Returns `{:ok, solution}` with `:references`, per-epoch `:epochs`, and the
carried `:final_state`, or `{:error, reason}`. Each epoch struct includes
`:geometry_quality`.

# `solve_rtk_fixed`

```elixir
@spec solve_rtk_fixed(map()) ::
  {:ok, Sidereon.GNSS.RTK.FixedBaselineSolution.t()} | {:error, term()}
```

Solve a static integer-fixed RTK baseline from normalized RTK epochs.

# `solve_rtk_float`

```elixir
@spec solve_rtk_float(map()) ::
  {:ok, Sidereon.GNSS.RTK.FloatBaselineSolution.t()} | {:error, term()}
```

Solve a static float RTK baseline from normalized RTK epochs.

# `solve_static_arc`

```elixir
@spec solve_static_arc([map()], Sidereon.GNSS.RTK.StaticArcConfig.t() | map()) ::
  {:ok, Sidereon.GNSS.RTK.StaticArcSolution.t()} | {:error, term()}
```

Solve a static RTK arc with a typed core-style configuration map.

The returned struct includes `:geometry_quality` for the static batch design.

# `solve_static_reference_station_rinex`

```elixir
@spec solve_static_reference_station_rinex(
  Sidereon.GNSS.SP3.t(),
  Sidereon.GNSS.RINEX.Observations.t(),
  Sidereon.GNSS.RINEX.Observations.t(),
  ecef_input(),
  keyword() | map()
) ::
  {:ok, Sidereon.GNSS.RTK.StaticReferenceStationSolution.t()} | {:error, term()}
```

Solve a static reference-station coordinate from paired RINEX OBS handles and SP3.

`reference_position_m` is the known reference receiver ECEF position in
metres. Options mirror `solve_static_rinex_rtk_baseline/5` for the carrier
path and also accept `:enable_code_dgnss`, `:enable_carrier_rtk`, and
`:with_geodetic`.

# `solve_static_rinex_rtk_baseline`

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

Solve a static RTK baseline directly from parsed RINEX OBS handles and SP3.

`base_m` is the base antenna reference point in ECEF metres. Options include
`:model`, `:reference`, `:max_epochs`, `:arc_options`, `:preprocessing`,
`:float_options`, `:fixed_options`, `:residual_options`, `:float_only_systems`,
`:initial_baseline_m`, and `:receiver_antenna_corrections`.

# `solve_wide_lane_fixed_rinex_rtk_baseline`

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

Solve a static dual-frequency wide-lane fixed RTK baseline from RINEX OBS and SP3.

The result contains the decoded static float and fixed solutions plus
`:wide_lane` metadata for the wide-lane integers used before the final
narrow-lane solve.

---

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