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

A single completed correlator output produced within one processing chunk.

track! processes each measurement in fixed-size time chunks. Every time a signal's coherent integration completes inside a chunk, the (raw, un-normalized) accumulator is snapshotted into a [CorrelatorOutput] and appended to that signal's correlator_outputs array; the Doppler estimator then folds over those records after the chunk. Storing the raw correlator plus integrated_samples lets the estimator normalize it (dividing by the sample count) and matches last_fully_integrated_correlator.

An external correlator producer (e.g. an FPGA) can build these itself and feed them to apply_record and step_loop directly, or to Tracking.jl's append_correlator_output!.

Fields:

  • correlator: the raw accumulated correlator at completion (not normalized). Fill it with the accumulator scaling normalize expects — the raw sum-of-products over integrated_samples — which an external producer gets for free by reusing EarlyPromptLateCorrelator / update_accumulator.
  • integrated_samples: samples integrated into this output (for normalize, the loop-filter integration_time, and the bit-buffer block count). For an external producer this is the true sample count of that integration.
  • sample_index: where this integration ended on the sample grid. As mean_nco_word measures spans, the record covers [sample_index - integrated_samples, sample_index), so a word landing at sample_index belongs to the next record. (Tracking.jl's software correlator stores the 1-based index of the record's last sample, which is the same number.) The Doppler estimators read it to look up the replica words the record ran on (mean_nco_word) and, for NCOReferencedPLLAndDLL, to place the record relative to the landing sample. It must therefore be on the same time grid as the words handed to step_loop: device samples for an NCOTimeline. For a FixedNCOWord the words are the same everywhere, so any consistent origin works — Tracking.jl's software correlator writes it relative to the current track! measurement, and an external producer feeding Tracking.jl must map its global counter onto that per-chunk origin too, so every satellite stays on one time grid for vector tracking.
  • code_phase: the replica's code phase (chips) at sample_index. The end sample alone pins the replica only to within one sample, so VectorPLLAndDLL reads the code phase from here. Only its part past the nearest code-block boundary is used, so any wrap convention works. NaN when the producer does not report it (the three-argument constructor), which only vector tracking needs.
source
TrackingLoops.normalize — Function
normalize(correlator, integrated_samples)
normalize(correlator, integrated_samples, code_amplitude)

Normalize the correlator by the number of integrated_samples and, optionally, the code_amplitude (the RMS amplitude of the sampled code replica; see get_code_amplitude). For a ±1 code code_amplitude is 1 and this is just the per-sample average; for a multi-level code (CBOC) dividing by code_amplitude additionally undoes the code's integer scale so the normalized prompt is independent of modulation.

source
TrackingLoops.EarlyPromptLateCorrelator — Type

EarlyPromptLateCorrelator holding a user defined number of correlation values. The code is shifted in samples. Hence, the specified code shift is actually a preferred code shift, because depending on sampling frequency and code frequency the specified code shift might not be the actual code shift. It is as close as possible, though. The algorithm makes sure that at least one sample is shifted.

source
TrackingLoops.get_correlator_sample_shifts — Method
get_correlator_sample_shifts(
    correlator,
    sampling_frequency,
    code_frequency
)

Calculate the replica phase offset required for the correlator with respect to the prompt correlator, expressed in samples. The shifts are ordered from latest to earliest replica.

source
TrackingLoops.VeryEarlyPromptLateCorrelator — Type

VeryEarlyPromptLateCorrelator holding a user defined number of correlation values. The code is shifted in samples. Hence, the specified code shift is actually a preferred code shift, because depending on sampling frequency and code frequency the specified code shift might not be the actual code shift. It is as close as possible, though. The algorithm makes sure that at least one sample is shifted.

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

VeryEarlyPromptLateCorrelator constructor without parameters and some default parameters. Default parameters take from https://gnss-sdr.org/docs/sp-blocks/tracking/#implementation-galileoe1dllpllveml_tracking

Throws an ArgumentError if both code shifts are one chip or more: the BOC(1,1) correlation peak the VEML discriminator (dll_disc) is calibrated on has vanished there, so no tap would see the signal.

source
TrackingLoops.get_correlator_sample_shifts — Method
get_correlator_sample_shifts(
    correlator,
    sampling_frequency,
    code_frequency
)

Calculate the replica phase offset required for the correlator with respect to the prompt correlator, expressed in samples. The shifts are ordered from latest to earliest replica.

source

Discriminators

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

Calculates the code phase error in chips using the noncoherent early minus late envelope normalized discriminator.

Uses the generalized normalization for arbitrary early-late spacing d (in chips): (2 - d) / 2 * (E - L) / (E + L) which reduces to 1/2 * (E - L) / (E + L) for the standard 1-chip spacing.

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
)

Calculates the code phase error in chips using 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), divided by its S-curve slope so the output is calibrated in chips — the contract the early-prompt-late method above keeps with its (2 - d) / 2 factor. The slope (≈ 4.3 for the default ±0.15/±0.6 chip taps) is evaluated on the piecewise-linear sine-BOC(1,1) autocorrelation envelope at the actual sample-quantized tap offsets, the calibration GNSS-SDR applies where it wants chips from a BOC discriminator (CalculateSlopeAbs on SinBocCorrelationFunction). Against the full CBOC/TMBOC modulations a residual gain error of the modulation mismatch remains, and the discriminator is linear only within the inner tap offset.

Throws an ArgumentError if both tap offsets, after quantization to whole samples, are one chip or more: the envelope is zero at both, so the slope is undefined. The correlator's constructor rejects such shifts already; this catches preferred shifts just below one chip that a coarse sampling rate rounds up to it.

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
)

Calculates the carrier frequency error in Hz.

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