Correlators and discriminators

A correlator holds the accumulators of one integration: one complex value per tap and antenna. EarlyPromptLateCorrelator has the early, prompt and late taps a DLL needs; VeryEarlyPromptLateCorrelator adds a very-early and a very-late tap, e.g. for multipath monitoring or BOC tracking. A CorrelatorOutput is one completed record as a correlator hands it to the loop: the correlator together with the sample span it was integrated over.

The discriminators pll_disc, fll_disc and dll_disc turn a (filtered) correlator into the carrier-phase, carrier-frequency and code-phase errors the loop filters act on. Before that, the DefaultPostCorrFilter combines the antennas of a multi-antenna correlator into one; subtype AbstractPostCorrFilter for beamforming.

Correlators

TrackingLoops.CorrelatorOutput — Type

One completed coherent integration (a record): the raw accumulator snapshotted when the integration completes. Tracking.jl's track! produces these; an external producer (e.g. an FPGA) can build them and feed apply_record and step_loop, or Tracking.jl's append_correlator_output!.

Fields:

  • correlator: the raw (un-normalized) sum of products, as normalize expects; an external producer gets it by reusing update_accumulator on an EarlyPromptLateCorrelator.
  • integrated_samples: samples integrated (for normalize, the integration time and the bit-buffer block count).
  • sample_index: where the integration ended; the record covers [sample_index - integrated_samples, sample_index) (numerically Tracking.jl's 1-based index of the record's last sample). Used to look up the words the replica ran on (mean_nco_word) and, for NCOReferencedPLLAndDLL, to place the record against the landing sample, so it must share the time grid of the words passed to step_loop: device samples for an NCOTimeline. With a FixedNCOWord any origin works as long as all satellites share it (vector tracking); Tracking.jl uses the current track! measurement, so an external producer feeding it must map its counter onto that origin.
  • code_phase: the replica's code phase (chips) at sample_index, read by VectorPLLAndDLL since the end sample pins it only to one sample. Only the part past the nearest code-block boundary is used, so any wrap convention works. NaN when not reported (three-argument constructor); only vector tracking needs it.
source
TrackingLoops.normalize — Function
normalize(correlator, integrated_samples)
normalize(correlator, integrated_samples, code_amplitude)

Normalize the correlator by integrated_samples times code_amplitude (the RMS amplitude of the sampled code replica, get_code_amplitude; 1 for a ±1 code), so the normalized prompt is independent of modulation (e.g. CBOC).

source
TrackingLoops.VeryEarlyPromptLateCorrelator — Method
VeryEarlyPromptLateCorrelator(
;
    num_ants,
    preferred_early_late_to_prompt_code_shift,
    preferred_very_early_late_to_prompt_code_shift
)

Construct a zeroed VeryEarlyPromptLateCorrelator; default shifts from GNSS-SDR's Galileo E1 VEML tracking: https://gnss-sdr.org/docs/sp-blocks/tracking/#implementation-galileo_e1_dll_pll_veml_tracking

Throws an ArgumentError if both code shifts are one chip or more, where the BOC(1,1) peak the VEML dll_disc is calibrated on has vanished.

source

Discriminators

TrackingLoops.dll_disc — Method
dll_disc(
    signal,
    correlator,
    code_doppler,
    sampling_frequency
)

Code phase error in chips from the noncoherent early-minus-late envelope normalized discriminator (2 - d) / 2 * (E - L) / (E + L) for early-late spacing d in chips (1/2 * (E - L) / (E + L) at 1 chip).

See: Kaplan & Hegarty, "Understanding GPS: Principles and Applications", 2nd ed., Table 5.5; GNSS-SDR tracking_discriminators.cc.

source
TrackingLoops.dll_disc — Method
dll_disc(
    signal,
    correlator,
    code_doppler,
    sampling_frequency
)

Code phase error in chips from the noncoherent very-early-minus-late envelope normalized discriminator (VE + E - VL - L) / (VE + E + VL + L) for BOC(1,1)-dominant signals (Galileo E1, GPS L1C, BeiDou B1C), divided by its S-curve slope so the output is in chips. The slope (4 / (2 − 3·0.15 − 0.6) ≈ 4.2 for the default ±0.15/±0.6 chip taps) is evaluated on the piecewise-linear sine-BOC(1,1) envelope at the sample-quantized tap offsets, as GNSS-SDR's CalculateSlopeAbs on SinBocCorrelationFunction does. Against full CBOC/TMBOC a residual gain error remains; the discriminator is linear only within the inner tap offset.

Throws an ArgumentError if both quantized tap offsets are one chip or more (slope undefined), which a coarse sampling rate can cause for shifts just below one chip.

Raw discriminator form from: https://gnss-sdr.org/docs/sp-blocks/tracking/#implementation-galileoe1dllpllveml_tracking

source
TrackingLoops.fll_disc — Method
fll_disc(
    signal,
    correlator,
    previous_prompt,
    integration_time;
    polarity
)

Carrier frequency error in Hz, whatever unit the integration time carries, from the rotation between the previous and current prompt; zero without a previous prompt. With polarity = 0 (default) the two-quadrant atan(cross / dot), insensitive to a sign flip between the prompts, range ±1 / (4 · integration_time). With polarity = ±1 the four-quadrant atan(cross, dot), range ±1 / (2 · integration_time); it needs both prompts to share their sign, as for pll_disc. Which sign does not matter (it cancels in the product), only whether polarity is nonzero.

See: Kaplan & Hegarty, "Understanding GPS: Principles and Applications", 2nd ed., Table 5.4.

source
TrackingLoops.pll_disc — Method
pll_disc(signal, correlator; polarity)

Carrier phase error in cycles, the unit the carrier loop filter takes alongside the FLL's Hz. With polarity = 0 (default) it is the two-quadrant Costas discriminator atan(Q / I) / 2π: insensitive to data or secondary-code sign flips, range ±1/4 cycle. With the prompt's sign as polarity (±1) it is the four-quadrant atan(Q, I) / 2π of polarity * prompt, range ±1/2 cycle, worth up to 6 dB of tracking threshold. A wrong polarity reads as half a cycle of error, so pass ±1 only where the prompt's sign is known: a dataless signal, synced to its secondary code.

See: Kaplan & Hegarty, "Understanding GPS: Principles and Applications", 2nd ed., Tables 5.2 and 5.3.

source

Post-correlation filter

TrackingLoops.AbstractPostCorrFilter — Type

Abstract post correlation filter for the prompt signal — the linear combiner that reduces a correlator's per-antenna taps to the single complex value the discriminators and the C/N₀ estimator see.

An implementation must provide two methods:

  • get_weights(filter, ::NumAnts{M}) — the combining weights w this filter currently applies. A plain ComplexF64 at M == 1, an SVector{M,ComplexF64} above.
  • update(filter, prompt) — return the filter to use for the next record, which is where an adaptive beamformer recomputes its weights.

The contract is deliberately linear in the antennas: the combined tap is wᴴ·b. That is what lets the C/N₀ path stay honest without any per-satellite noise state. The noise reference measures one spatial covariance R̂ per signal (see AbstractNoiseEstimator), and each satellite reduces it to its own scalar floor through its own weights, N₀ = wᴴR̂w — exact for any fixed w, because E[|wᴴn|²] = wᴴRw. A filter that combined its antennas non-linearly would have no such closed form, and its C/N₀ would silently be a ratio of two different noise scales.

source
TrackingLoops.DefaultPostCorrFilter — Type

This is the default post correlation filter. For a single antenna channel it returns the prompt value as is. For multi antenna systems it selects the last antenna's value — the noise reference's covariance reduces to that antenna's own floor under these weights, so the two sides of the C/N₀ ratio agree.

source
TrackingLoops.get_weights — Function

The linear combining weights w that filter currently applies to a correlator tap, as a ComplexF64 for NumAnts(1) and an SVector{M,ComplexF64} for NumAnts(M). The combined tap is wᴴ·b, and the matching post-combination noise floor is wᴴR̂w — see AbstractPostCorrFilter.

Required for every AbstractPostCorrFilter; there is no fallback, because a filter whose weights are unknown cannot be given a correct C/N₀.

Examples

A beamformer that averages the antenna elements:

struct MyBeamformer <: AbstractPostCorrFilter end
TrackingLoops.update(f::MyBeamformer, prompt) = f
TrackingLoops.get_weights(::MyBeamformer, ::NumAnts{1}) = 1.0 + 0.0im
TrackingLoops.get_weights(::MyBeamformer, ::NumAnts{M}) where {M} =
    SVector{M,ComplexF64}(ntuple(_ -> 1 / M + 0.0im, M))
source
TrackingLoops.update — Method
update(filter, prompt)

Update the post-correlation filter with the latest prompt and return the new filter (immutable update). This is the extension point for custom AbstractPostCorrFilters — e.g. a beamformer adapting its weights.

source