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 early, prompt and late taps (get_early, get_prompt, get_late), e.g. EarlyPromptLateCorrelator and VeryEarlyPromptLateCorrelator.
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, asnormalizeexpects; an external producer gets it by reusingupdate_accumulatoron anEarlyPromptLateCorrelator.integrated_samples: samples integrated (fornormalize, 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, forNCOReferencedPLLAndDLL, to place the record against the landing sample, so it must share the time grid of thewordspassed tostep_loop: device samples for anNCOTimeline. With aFixedNCOWordany origin works as long as all satellites share it (vector tracking); Tracking.jl uses the currenttrack!measurement, so an external producer feeding it must map its counter onto that origin.code_phase: the replica's code phase (chips) atsample_index, read byVectorPLLAndDLLsince 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.NaNwhen not reported (three-argument constructor); only vector tracking needs it.
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 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).
TrackingLoops.EarlyPromptLateCorrelator — Type
Correlator with early, prompt and late taps. The code shift (chips) is a preferred shift: rounded to whole samples for the sampling and code frequency, at least one.
TrackingLoops.EarlyPromptLateCorrelator — Method
EarlyPromptLateCorrelator(
;
num_ants,
preferred_early_late_to_prompt_code_shift
)
Construct a zeroed EarlyPromptLateCorrelator.
TrackingLoops.get_correlator_sample_shifts — Method
get_correlator_sample_shifts(
correlator,
sampling_frequency,
code_frequency
)
Replica shifts relative to the prompt in samples, from latest to earliest replica.
TrackingLoops.update_accumulator — Method
update_accumulator(correlator, accumulators)
Update the correlator with new accumulators
TrackingLoops.VeryEarlyPromptLateCorrelator — Type
Correlator with very-early, early, prompt, late and very-late taps. Code shifts are preferred shifts, rounded to whole samples as for EarlyPromptLateCorrelator.
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.
TrackingLoops.get_correlator_sample_shifts — Method
get_correlator_sample_shifts(
correlator,
sampling_frequency,
code_frequency
)
Replica shifts relative to the prompt in samples, 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
)
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.
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
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.
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.
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.