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

Dual-frequency carrier-phase linear combinations and the precise-positioning
prep tooling built on them: geometry-free and wide-lane phase, the
narrow-lane code, Melbourne-Wubbena, cycle-slip detection, and Hatch
carrier-smoothed code.

The numerical modeling lives in the Rust core. This module keeps the public
Elixir API shape, validates options, normalizes arc maps for the NIF, and maps
the core results back to the documented return maps.

## Arc shape

`detect_cycle_slips/2` and `smooth_code/2` take an `arc`: a time-ordered list
of per-epoch maps for **one** satellite. Each epoch map is a convenient subset
of

    %{
      epoch: term(),        # opaque, passed through to the output
      phi1: float | nil,    # band-1 carrier phase, cycles
      phi2: float | nil,    # band-2 carrier phase, cycles
      p1:   float | nil,    # band-1 code, metres
      p2:   float | nil,    # band-2 code, metres
      lli1: integer | nil,  # band-1 LLI (bit 0 = loss of lock)
      lli2: integer | nil,  # band-2 LLI
      f1:   float | nil,    # band-1 carrier frequency, Hz (nil => skip)
      f2:   float | nil     # band-2 carrier frequency, Hz
    }

An epoch with an unknown band frequency is skipped and reported with
`skipped: true`. `epoch` is passed through unchanged. The data-gap detector
can compare numeric-second epochs and `NaiveDateTime` epochs; other epoch
terms remain opaque and do not trigger `:data_gap`.

# `epoch_map`

```elixir
@type epoch_map() :: %{optional(atom()) =&gt; term()}
```

# `iono_free_smooth_result`

```elixir
@type iono_free_smooth_result() :: %{
  epoch: term(),
  p_smooth: float() | nil,
  p_if: float() | nil,
  l_if: float() | nil,
  window: non_neg_integer(),
  reset: boolean()
}
```

# `slip_reason`

```elixir
@type slip_reason() :: :lli | :geometry_free | :melbourne_wubbena | :data_gap
```

# `slip_result`

```elixir
@type slip_result() :: %{
  epoch: term(),
  slip: boolean(),
  reasons: [slip_reason()],
  gf: float() | nil,
  mw: float() | nil,
  skipped: boolean()
}
```

# `smooth_result`

```elixir
@type smooth_result() :: %{
  epoch: term(),
  p_smooth: float() | nil,
  window: non_neg_integer(),
  reset: boolean()
}
```

# `code_minus_carrier`

```elixir
@spec code_minus_carrier(number(), number(), number()) ::
  {:ok, float()} | {:error, :invalid_frequency}
```

Code-minus-carrier diagnostic `CMC = P - L` (metres).

# `detect_cycle_slips`

```elixir
@spec detect_cycle_slips(
  [epoch_map()],
  keyword()
) :: [slip_result()]
```

Detect cycle slips on a single-satellite arc.

Returns one output map per input epoch:

    %{epoch: term(), slip: boolean(), reasons: [reason],
      gf: float | nil, mw: float | nil, skipped: boolean()}

with `reason in [:lli, :geometry_free, :melbourne_wubbena, :data_gap]`.

Options:

  * `:gf_threshold_m` (default `0.05`)
  * `:mw_threshold_cycles` (default `4.0`)
  * `:min_arc_gap_s` (default `300.0`)

Thresholds must be non-negative numbers; invalid values raise
`ArgumentError`.

# `geometry_free`

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

Geometry-free phase combination `L_GF = l1_m - l2_m` (metres).

# `melbourne_wubbena`

```elixir
@spec melbourne_wubbena(number(), number(), number(), number(), number(), number()) ::
  {:ok, float()} | {:error, :equal_frequencies}
```

Melbourne-Wubbena combination (metres).

    MW = L_WL - P_NL
       = lambda_WL*(phi1 - phi2) - (f1*P1 + f2*P2)/(f1 + f2)

Sign convention: wide-lane phase minus narrow-lane code.

# `narrow_lane_code`

```elixir
@spec narrow_lane_code(number(), number(), number(), number()) ::
  {:ok, float()} | {:error, :equal_frequencies}
```

Narrow-lane code `P_NL = (f1*p1 + f2*p2) / (f1 + f2)` (metres).

# `phase_meters`

```elixir
@spec phase_meters(number(), number()) ::
  {:ok, float()} | {:error, :invalid_frequency}
```

Carrier phase in metres, `L = c / f * phi`.

`phi_cyc` is carrier phase in cycles and `f_hz` is the carrier frequency in
hertz. Returns `{:ok, l_m}` or `{:error, :invalid_frequency}` when the
frequency is not positive. Never raises.

# `smooth_code`

```elixir
@spec smooth_code(
  [epoch_map()],
  keyword()
) :: [smooth_result()]
```

Single-frequency Hatch carrier-smoothed code on band 1.

The filter resets on detected cycle slips, LLI loss of lock, data gaps, or
missing band-1 code/phase/frequency. `:hatch_window_cap` defaults to
`100` and must be a positive integer.

# `smooth_iono_free_code`

```elixir
@spec smooth_iono_free_code(
  [epoch_map()],
  keyword()
) :: [iono_free_smooth_result()]
```

Dual-frequency ionosphere-free Hatch carrier-smoothed code.

Forms ionosphere-free code and carrier phase at each epoch in the Rust core,
then applies the same Hatch recursion and reset policy as `smooth_code/2`.

# `wide_lane_cycles`

```elixir
@spec wide_lane_cycles(number(), number(), number(), number(), number(), number()) ::
  {:ok, float()} | {:error, :equal_frequencies}
```

Melbourne-Wubbena wide-lane ambiguity estimate in wide-lane cycles.

Computes `melbourne_wubbena/6` divided by the wide-lane wavelength. The
result is cycle-normalized and uses the same sign convention and error
behavior as `melbourne_wubbena/6`.

# `wide_lane_wavelength`

```elixir
@spec wide_lane_wavelength(number(), number()) ::
  {:ok, float()} | {:error, :equal_frequencies}
```

Wide-lane wavelength `lambda_WL = c / (f1 - f2)` (metres).

Returns `{:ok, lambda_WL}` or `{:error, :equal_frequencies}` when the two
band frequencies are equal within the core epsilon.

---

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