Vector Tracking

VectorPLLAndDLL is a Doppler estimator for vector tracking (a vector delay/frequency-lock loop, VDFLL). Where the ConventionalPLLAndDLL closes each satellite's carrier and code loops independently with per-satellite loop filters, vector tracking closes them centrally: an external navigation filter — living outside Tracking.jl, e.g. in GNSSReceiver.jl — combines every satellite's measurements with the receiver dynamics and feeds back per-satellite NCO corrections. A satellite in a deep fade keeps being steered by the solution the healthy satellites constrain, which is what gives vector tracking its robustness under weak signal and high dynamics.

Tracking.jl owns only the signal-path half of that loop: it correlates, forms discriminators, and applies the corrections the navigation filter hands back. VectorPLLAndDLL is the interface between the two.

The per-integration contract

VectorPLLAndDLL is configuration-only; the per-satellite state lives on each TrackedSat as a SatVectorPLLAndDLL (seeded through init_estimator_state, like every estimator — see Custom Doppler Estimator). Each time a satellite's estimator-driver signal (signals[1]) completes an integration, VectorPLLAndDLL does one of two things depending on that satellite's vt_on flag:

  • vt_on = false — scalar fallback. The satellite runs an ordinary FLL-assisted PLL and DLL, identical to ConventionalAssistedPLLAndDLL (same default loop-filter types and the same auto-sized loop bandwidths). This is the pull-in mode a freshly acquired satellite tracks in until enable_vt! puts it into the vector loop.

  • vt_on = true — vector closure. The navigation filter drives the NCOs instead of the local loop filters:

    • the code Doppler follows code_freq_update directly (the DLL loop filter is bypassed and holds its state);
    • the carrier's FLL branch is driven by carrier_freq_update while the PLL branch still runs on the satellite's own phase discriminator — so the carrier is a vector frequency lock loop with a retained local phase lock. (This needs the FLL-assisted ThirdOrderAssistedBilinearLF, the default carrier filter; with a plain PLL filter the carrier_freq_update has no input path and the vector carrier closure degrades to PLL-only.)

    In this mode the DLL and FLL discriminator outputs and the prompt magnitude are accumulated on the per-sat state for the navigation filter to read and reset.

The satellite-shared carrier/code Doppler is always updated through the same carrier-aiding (aid_dopplers) used by the conventional estimator, and the same effective-bandwidth handling applies when a signal integrates N primary code blocks coherently: 1/N on the carrier loop, a stability cap against the actual integration time on the code loop.

The receiver-side loop

A navigation filter drives VectorPLLAndDLL through the exported state managers. A typical iteration, after track! has produced new correlations:

# 1. Put the satellites that have pulled in into the vector loop.
#    `prns_in_lock` is whatever the receiver decides is usable (e.g. a
#    C/N0 threshold); re-issuing the same set every iteration is a no-op.
enable_vt!(track_state, prns_in_lock)

# 2. Read the accumulated discriminator outputs the estimator collected
#    for each vector-loop satellite, then reset the accumulators so the
#    next block accumulates afresh.
for (prn, sat) in pairs(get_sat_states(track_state))
    state = get_doppler_estimator_state(sat)
    state.vt_on || continue
    code_err = mean_code_discr(state)      # chips, or `nothing` if no data
    carrier_err = mean_carrier_discr(state) # Hz, or `nothing` if no data
    # … feed the mean measurements into the navigation filter …
end
reset_code_discr_acc!(track_state)
reset_carrier_discr_acc!(track_state)

# 3. Run the navigation filter, then feed its per-satellite NCO
#    corrections back. Both setters take anything indexable by PRN
#    (e.g. a Dictionaries.Dictionary) with an entry for every vector-loop
#    satellite; satellites still in the scalar fallback are skipped.
set_code_freq_updates!(track_state, code_freq_updates)
set_carrier_freq_updates!(track_state, carrier_freq_updates)

The accumulators are stored as (count, sum) tuples; mean_code_discr / mean_carrier_discr apply the averaging convention (sum / count, returning nothing when nothing has accumulated) in one place, so consumers don't each re-implement the divide and the count == 0 guard. Reading and resetting are deliberately separate calls so the filter can read at its own (typically slower) rate than track!.

Multi-constellation addressing

Each state manager has an all-groups form and a group-scoped form that takes a group selector (Symbol, Integer, or Val) as its first argument after track_state:

enable_vt!(track_state, :gps, gps_prns_in_lock)
set_code_freq_updates!(track_state, :galileo, galileo_code_updates)

In a multi-constellation receiver a PRN alone is ambiguous — GPS PRN 5 and Galileo PRN 5 are different satellites in different groups — so address each constellation's group explicitly. The all-groups form matches a PRN in every group and is only unambiguous for a single-group TrackState.

Loop membership

enable_vt! and disable_vt! are the only way loop membership changes; the estimator never joins or drops a satellite on its own. Membership is not a lock indicator: a satellite in an outage stays in the loop and keeps being steered by the navigation filter from the shared solution. Deciding whether its (now uninformative) discriminator outputs should feed the measurement update is the navigation filter's responsibility, tracked on the receiver side rather than on the estimator state — disable_vt! is for satellites the filter gives up on entirely, and wants handed back to their own scalar loop (follow it with reset_loop_filters! for a transient-free handoff).

Resetting

reset_loop_filters! zeroes the loop-filter integrators, the discriminator accumulators, and the NCO corrections, re-seeding the loop from the satellite's current (converged) Doppler while preserving the vt_on flag and any per-satellite bandwidth override. The NCO corrections are zeroed because the re-seeded Doppler already contains the last correction — keeping it would apply it twice. After a reset the navigation filter must re-issue its corrections via set_code_freq_updates! / set_carrier_freq_updates! before the next vector-closed integration.

API reference

Tracking.VectorPLLAndDLLType

Vector-tracking Phase-Locked Loop (PLL) and Delay-Locked Loop (DLL) Doppler estimator. Configuration-only — per-satellite state lives in each TrackedSat wrapper as a SatVectorPLLAndDLL, produced via init_estimator_state.

In vector tracking, the per-satellite tracking loops are closed centrally by a navigation filter (living outside this package, e.g. in GNSSReceiver.jl) instead of by per-satellite loop filters. The division of labor per integration:

  • This estimator accumulates each satellite's DLL / FLL discriminator outputs for the navigation filter to consume (and reset via reset_code_discr_acc! / reset_carrier_discr_acc!).
  • The navigation filter feeds NCO corrections back via set_code_freq_updates! / set_carrier_freq_updates!. While a satellite's vt_on flag is set, its code Doppler follows the navigation filter's code_freq_update directly and the FLL branch of the FLL-assisted carrier loop filter is driven by the navigation filter's carrier_freq_update (a vector delay / frequency lock loop) while the PLL branch still runs on the satellite's own discriminator.
  • Satellites with vt_on unset (fresh from acquisition, before enable_vt! puts them in the loop) run a conventional scalar PLL/DLL as a fallback.

Type parameters CA and CO select the carrier and code loop filter types. The FLL-assisted ThirdOrderAssistedBilinearLF carrier filter is the default — with any non-assisted filter the navigation filter's carrier_freq_update has no input path into the carrier loop.

Each bandwidth field is Maybe{typeof(1.0Hz)}: a nothing field (the default) means autoinit_estimator_state sizes the bandwidth per satellite from that sat's estimator-driver signal (signals[1]) via default_carrier_loop_filter_bandwidth / default_code_loop_filter_bandwidth — the same sizing as the conventional estimator, which the scalar fallback loop is. Like the conventional estimator, the effective bandwidth is scaled by 1/N at filter time when a signal coherently integrates N primary code blocks.

source
Tracking.SatVectorPLLAndDLLType

Per-satellite state for the vector PLL and DLL Doppler estimator (VectorPLLAndDLL).

On top of the conventional loop-filter state it carries the vector-tracking (VT) interface to an external navigation filter (e.g. GNSSReceiver.jl's VDFLL):

  • code_discr_acc / carrier_discr_acc: (count, sum) accumulators of the DLL discriminator (chips) and the FLL discriminator (Hz) since the navigation filter last read and reset them (reset_code_discr_acc! / reset_carrier_discr_acc!). Only accumulated while vt_on.
  • code_freq_update / carrier_freq_update: the NCO corrections the navigation filter feeds back (set_code_freq_updates!, set_carrier_freq_updates!). While vt_on, they replace the scalar DLL loop-filter output and the FLL branch of the carrier loop filter respectively.
  • vt_on: whether the navigation filter controls this satellite's NCOs. While false the satellite runs a conventional (scalar) PLL/DLL as a fallback and nothing is accumulated. Set by enable_vt! / disable_vt!.
source
Tracking.enable_vt!Function
enable_vt!(track_state, prns)

Put every satellite whose PRN is in prns into the vector loop by setting its vt_on flag: from the next integration on, the navigation filter's NCO corrections drive its loops (set_code_freq_updates! / set_carrier_freq_updates!) and its DLL/FLL discriminators are accumulated for the filter to read. Satellites outside prns are left untouched, and re-enabling one already in the loop changes nothing — pass the currently usable set (e.g. the satellites in lock) every cycle rather than only the newly eligible ones.

The two-argument form walks every group; pass a group (Symbol or index) to address a single group — required in a multi-constellation receiver, where a PRN alone is ambiguous across groups.

Mutates track_state in place and returns it. disable_vt! is the inverse.

source
Tracking.disable_vt!Function
disable_vt!(track_state, prns)

Take every satellite whose PRN is in prns out of the vector loop by clearing its vt_on flag — the inverse of enable_vt!, for satellites the navigation filter drops (unhealthy, diverged, no longer visible). Each falls back to its own scalar PLL/DLL, whose loop filters still carry their pre-vector state and whose Doppler now contains the navigation filter's last NCO correction; follow up with reset_loop_filters! to re-seed the scalar loop from the current Doppler and make the handoff transient-free.

Addressed exactly like enable_vt!: every group, or only group when one is given. Mutates track_state in place and returns it.

source
Tracking.set_code_freq_updates!Function
set_code_freq_updates!(track_state, code_freq_updates)

Feed the navigation filter's code-frequency NCO corrections back into the tracking loops. code_freq_updates maps PRN to the correction (anything indexable by PRN, e.g. a Dictionary) and must have an entry for every satellite in the vector loop; satellites with vt_on unset are skipped. Walks every group, or only group when one is given — the group-scoped form is required in a multi-constellation receiver, where each group carries its own corrections and PRNs collide across groups. Mutates track_state in place and returns it.

source
Tracking.set_carrier_freq_updates!Function
set_carrier_freq_updates!(track_state, carrier_freq_updates)

Feed the navigation filter's carrier-frequency NCO corrections back into the tracking loops. carrier_freq_updates maps PRN to the correction (anything indexable by PRN, e.g. a Dictionary) and must have an entry for every satellite in the vector loop; satellites with vt_on unset are skipped. Walks every group, or only group when one is given — the group-scoped form is required in a multi-constellation receiver, where each group carries its own corrections and PRNs collide across groups. Mutates track_state in place and returns it.

source
Tracking.reset_code_discr_acc!Function
reset_code_discr_acc!(track_state)

Reset the code (DLL) discriminator accumulator of every satellite in the vector loop — called by the navigation filter after it has consumed the accumulated values via mean_code_discr. Satellites outside the vector loop are left untouched (they accumulate nothing).

The one-argument form walks every group; pass a group (Symbol or index) to reset a single group's satellites. Mutates track_state in place and returns it.

source
Tracking.reset_carrier_discr_acc!Function
reset_carrier_discr_acc!(track_state)

Reset the carrier (FLL) discriminator accumulator of every satellite in the vector loop — called by the navigation filter after it has consumed the accumulated values via mean_carrier_discr. Addressed exactly like reset_code_discr_acc!: every group, or only group when one is given. Mutates track_state in place and returns it.

source
Tracking.mean_code_discrFunction
mean_code_discr(state)

Mean DLL (code) discriminator accumulated on state since the last reset_code_discr_acc!, in chips, or nothing if nothing has been accumulated yet (the (count, sum) accumulator's count is 0). This is the single place the accumulator's averaging convention lives — read it here rather than dividing code_discr_acc by hand, so a change to how the accumulator is stored can't silently diverge between consumers.

source