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.AbstractCorrelator — Type
AbstractCorrelator{M}Abstract supertype for the accumulators of one integration, for M antennas. A subtype holds one complex accumulator per tap (and antenna) and answers get_accumulators, get_prompt, get_num_ants, get_correlator_sample_shifts and the other accessors.
TrackingLoops.AbstractEarlyPromptLateCorrelator — Type
AbstractEarlyPromptLateCorrelator{M} <: AbstractCorrelator{M}Abstract supertype for correlators with an early, a prompt and a late tap — the taps the discriminators read (get_early, get_prompt, get_late). EarlyPromptLateCorrelator and VeryEarlyPromptLateCorrelator are its subtypes.
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 scalingnormalizeexpects — the raw sum-of-products overintegrated_samples— which an external producer gets for free by reusingEarlyPromptLateCorrelator/update_accumulator.integrated_samples: samples integrated into this output (fornormalize, the loop-filterintegration_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. Asmean_nco_wordmeasures spans, the record covers[sample_index - integrated_samples, sample_index), so a word landing atsample_indexbelongs 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, forNCOReferencedPLLAndDLL, to place the record relative to the landing sample. It must therefore be on the same time grid as thewordshanded tostep_loop: device samples for anNCOTimeline. For aFixedNCOWordthe words are the same everywhere, so any consistent origin works — Tracking.jl's software correlator writes it relative to the currenttrack!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) atsample_index. The end sample alone pins the replica only to within one sample, soVectorPLLAndDLLreads the code phase from here. Only its part past the nearest code-block boundary is used, so any wrap convention works.NaNwhen the producer does not report it (the three-argument constructor), which only vector tracking needs.
TrackingLoops.get_accumulators — Method
get_accumulators(correlator)
Get all correlator accumulators
TrackingLoops.get_early — Method
get_early(correlator)
Get early correlator
TrackingLoops.get_early_late_sample_spacing — Method
get_early_late_sample_spacing(
correlator,
sampling_frequency,
code_frequency
)
Calculate the total spacing between early and late correlator in samples.
TrackingLoops.get_late — Method
get_late(correlator)
Get late correlator
TrackingLoops.get_num_accumulators — Method
get_num_accumulators(correlator)
Get number of accumulators
TrackingLoops.get_num_ants — Method
get_num_ants(correlator)
Get number of antennas from correlator
TrackingLoops.get_prompt — Method
get_prompt(correlator)
Get prompt correlator
TrackingLoops.get_prompt_index — Method
get_prompt_index(correlator)
Get prompt correlator index
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.
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.
TrackingLoops.EarlyPromptLateCorrelator — Method
EarlyPromptLateCorrelator(
;
num_ants,
preferred_early_late_to_prompt_code_shift
)
EarlyPromptLateCorrelator constructor.
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.
TrackingLoops.update_accumulator — Method
update_accumulator(correlator, accumulators)
Update the correlator with new accumulators
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.
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.
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.
TrackingLoops.get_very_early — Method
get_very_early(correlator)
Get very early correlator
TrackingLoops.get_very_late — Method
get_very_late(correlator)
Get very late correlator
TrackingLoops.update_accumulator — Method
update_accumulator(correlator, accumulators)
Update the correlator with new accumulators
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.
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
TrackingLoops.fll_disc — Method
fll_disc(
signal,
correlator,
previous_prompt,
integration_time
)
Calculates the carrier frequency error in Hz.
TrackingLoops.pll_disc — Method
pll_disc(signal, correlator)
Calculates the carrier phase error in radians.
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 weightswthis filter currently applies. A plainComplexF64atM == 1, anSVector{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.
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.
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))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.