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

Baseband simulation, correlation, and acquisition for the GPS L1 C/A signal.

This module builds the "generate a replica, correlate, and acquire a PRN"
layer on top of `Sidereon.GNSS.Signal.CA`. It works entirely in complex baseband
(the carrier has already been removed down to a residual Doppler), using the
standard time-domain signal-processing model described in the GNSS
acquisition literature:

  * Kaplan & Hegarty, *Understanding GPS/GNSS: Principles and Applications*
    (3rd ed.), Ch. 5-8 (signal acquisition, coherent correlation, the
    sinc Doppler-mismatch loss, and the C/N0 to post-correlation SNR
    relation).
  * Misra & Enge, *Global Positioning System: Signals, Measurements, and
    Performance* (2nd ed.), Ch. 10-11.
  * Borre, Akos, Bertelsen, Rinder & Jensen, *A Software-Defined GPS and
    Galileo Receiver* (the 2D code-phase by Doppler search and the
    peak-to-noise acquisition metric).

## Sampled-code replica (zero-order-hold / nearest-chip)

The 1023-chip C/A code is sampled to a sampling rate `fs` over an integration
time `T` (so `N = round(fs * T)` samples). At sample `n` the code phase, in
chips, advances at the code rate:

    code_rate = f_chip * (1 + fd_code / f_L1)
    sampled[n] = chip(prn, floor((code_phase + n * code_rate / fs) mod 1023))

with `f_chip = 1_023_000` cps and `f_L1 = 1_575_420_000` Hz. `code_phase` is
the initial offset in chips and `fd_code` is an optional code-rate Doppler
scaling (defaults to `0.0`; code Doppler is negligible over one 1 ms period
but the parameter is exposed). This is a clean nearest-chip (zero-order-hold)
sampler: it picks the chip the sample instant falls within. `CA.chip/2`
already wraps its index modulo 1023, so the sampler wraps for free.

## Coherent correlation

For a complex baseband record `x[n] = xI[n] + j*xQ[n]` (a real-valued test
signal has `xQ = 0`), correlation against a local carrier wipe-off
`exp(-j 2*pi*f_d*n/fs)` times the real bipolar code `c[n]` is the coherent
sum over the integration window:

    S = sum_{n=0}^{N-1} x[n] * c[n] * exp(-j 2*pi*f_d*n/fs)
    I = sum ( xI[n]*c[n]*cos(th_n) + xQ[n]*c[n]*sin(th_n) )
    Q = sum ( xQ[n]*c[n]*cos(th_n) - xI[n]*c[n]*sin(th_n) ),  th_n = 2*pi*f_d*n/fs
    power = I^2 + Q^2

The amplitude is recovered as `sqrt(I^2 + Q^2)`; for a real input the carrier
wipe-off still spreads energy across both I and Q so the magnitude is the
meaningful quantity.

## Acquisition metric

`acquire/3` performs a 2D search over code-phase bins (at sample resolution)
and Doppler bins, computing the correlator `power` on each cell. The detection
metric is the standard peak-to-noise ratio used in software receivers:

    metric = peak_power / mean(off_peak_powers)

where the off-peak set excludes the peak cell and an exclusion zone of one
code-phase bin on either side of the peak (at the peak's Doppler bin), so the
main correlation lobe is not mistaken for the noise floor. For a clean signal
this metric is large (order of the number of samples); for noise or the wrong
PRN it is close to one. The alternative peak-to-second-peak ratio is also a
standard choice; this module exposes peak-to-mean-off-peak as `metric`.

## Coherent integration loss and post-correlation SNR

A residual frequency error `f` over a coherent integration time `T` attenuates
the correlation by the sinc-squared Doppler-mismatch loss (Kaplan & Hegarty,
the residual-carrier loss whose discrete form
`|sin(N*pi*df*Ts) / (N*sin(pi*df*Ts))|` tends to `sinc(pi*f*T)` in the
continuous limit):

    coherent_loss(f, T) = sinc^2(pi*f*T) = ( sin(pi*f*T) / (pi*f*T) )^2

which is `1` (0 dB) at `f = 0` and has its first null at `f = 1/T`. The
correlation *amplitude* scales as `|sinc(pi*f*T)| = sqrt(coherent_loss)`.

For the post-correlation signal-to-noise ratio this module exposes only the
relation it can state cleanly from a standard reference: coherent integration
over `T` seconds gives the predetection SNR

    snr_post_db(cn0_dbhz, T) = cn0_dbhz + 10*log10(T)

i.e. the carrier-to-noise-density ratio C/N0 (dB-Hz) plus the processing gain
`10*log10(T)` corresponding to a `1/T` effective noise bandwidth (Kaplan &
Hegarty; Misra & Enge). The exact noise-bandwidth convention (`1/T` versus
`1/(2T)`) differs between texts; this module uses the `1/T` form. No second,
competing gain formula is exposed.

# `acquire`

```elixir
@spec acquire(list(), integer(), keyword()) ::
  {:ok, map()}
  | {:error, :empty_samples | :too_short | {:unsupported_prn, integer()}}
```

Acquires a PRN by a 2D search over code phase and Doppler.

`samples` is the complex baseband record (list of `{i, q}` tuples or bare
reals). Options:

  * `:sample_rate_hz` - default `2.046e6`.
  * `:doppler_min_hz` / `:doppler_max_hz` / `:doppler_step_hz` - the Doppler
    search grid (defaults `-2500`, `2500`, `500` Hz).

The code-phase axis is searched at sample resolution over one code period.
Returns

    {:ok, %{
      code_phase_chips: float,
      doppler_hz: float,
      peak_metric: float,
      metric: float,
      peak_power: float,
      grid: %{
        doppler_hz: [float],
        code_phase_bins: integer,
        doppler_step_hz: float,
        samples_per_chip: float
      }
    }}

`metric` (and its alias `peak_metric`) is the peak-to-mean-off-peak power
ratio described in the module docs. Errors:

  * `{:error, :empty_samples}` for an empty record,
  * `{:error, :too_short}` if the record is shorter than one code period,
  * `{:error, {:unsupported_prn, prn}}` propagated from the code generator.

# `coherent_loss`

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

Coherent integration loss from a residual frequency error.

Returns the linear loss `sinc^2(pi*f*T) = (sin(pi*f*T)/(pi*f*T))^2` in
`[0, 1]`: `1.0` at `f = 0`, with its first null at `f = 1/T`. The correlation
amplitude scales as `sqrt` of this value.

## Examples

    iex> Sidereon.GNSS.Signal.Correlator.coherent_loss(0.0, 1.0e-3)
    1.0

# `coherent_loss_db`

```elixir
@spec coherent_loss_db(number(), number()) :: float() | :neg_infinity
```

Coherent integration loss in decibels, `10*log10(coherent_loss(f, T))`.

Returns `:neg_infinity` at an exact null (loss of zero).

# `correlate`

```elixir
@spec correlate(list(), integer(), keyword()) ::
  {:ok, %{i: float(), q: float(), power: float()}}
  | {:error, {:unsupported_prn, integer()} | :empty_samples}
```

Coherently correlates a complex baseband record against a PRN replica.

`iq` is a list of samples, each either a `{i, q}` tuple or a bare real number
(interpreted as `i` with `q = 0`). Options:

  * `:sample_rate_hz` - default `2.046e6`.
  * `:doppler_hz` - residual carrier Doppler to wipe off (default `0.0`).
  * `:code_phase_chips` - replica code phase offset in chips (default `0.0`).
  * `:code_doppler_hz` - replica code-rate Doppler (default `0.0`).

Returns `{:ok, %{i: i, q: q, power: i*i + q*q}}`, or propagates the PRN error.
The replica is generated at `length(iq)` samples to match the record.

# `correlate_against`

```elixir
@spec correlate_against(list(), [integer()], number(), number()) :: {float(), float()}
```

Low-level coherent correlation of a baseband record against an explicit
sampled `±1` code.

`iq` is a list of `{i, q}` tuples or bare reals; `code` is the sampled `±1`
vector (same length); `fs` is the sample rate and `doppler_hz` the residual
carrier to wipe off. Returns `{i, q}`, the real and imaginary parts of the
coherent sum.

# `replica`

```elixir
@spec replica(
  integer(),
  keyword()
) :: {:ok, [integer()]} | {:error, {:unsupported_prn, integer()}}
```

Builds a sampled `±1` C/A code replica.

Options:

  * `:sample_rate_hz` - sampling rate in Hz (default `2.046e6`, 2 samples/chip).
  * `:integration_time_s` - integration time in seconds (default one code
    period, `1023 / 1_023_000` = 1 ms). Determines `N` together with the
    sample rate, unless `:num_samples` is given.
  * `:num_samples` - explicit sample count `N` (overrides `:integration_time_s`).
  * `:code_phase_chips` - initial code phase offset in chips (default `0.0`).
  * `:code_doppler_hz` - code-rate Doppler scaling in Hz (default `0.0`).

Returns `{:ok, samples}` with `samples` a list of `±1` integers of length `N`,
or propagates `{:error, {:unsupported_prn, prn}}` from `Sidereon.GNSS.Signal.CA`.

## Examples

    iex> {:ok, s} = Sidereon.GNSS.Signal.Correlator.replica(1, num_samples: 4, sample_rate_hz: 1.023e6)
    iex> s
    [-1, -1, 1, 1]

# `snr_post_db`

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

Post-correlation (predetection) SNR in dB from C/N0 and integration time.

Uses the standard relation `snr_post_db = cn0_dbhz + 10*log10(T)` (Kaplan &
Hegarty; Misra & Enge), corresponding to a `1/T` effective noise bandwidth.

## Examples

    iex> Float.round(Sidereon.GNSS.Signal.Correlator.snr_post_db(40.0, 1.0e-3), 6)
    10.0

---

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