Loop Filter

The loop filters are provided by TrackingLoopFilters.jl. This includes:

  • first order loop filter FirstOrderLF
  • second order bilinear loop filter SecondOrderBilinearLF
  • second order boxcar loop filter SecondOrderBoxcarLF
  • third order bilinear loop filter ThirdOrderBilinearLF
  • third order boxcar loop filter ThirdOrderBoxcarLF
  • third order assisted bilinear loop filter ThirdOrderAssistedBilinearLF (combines PLL and FLL)

Default Configuration

The default Doppler estimator is ConventionalAssistedPLLAndDLL which uses:

  • ThirdOrderAssistedBilinearLF for the carrier loop (FLL-assisted PLL for improved dynamics)
  • SecondOrderBilinearLF for the code loop

When TrackState builds the default estimator implicitly from a signal-tuple declaration, the loop bandwidths are sized per signal at BL · T ≈ 0.018, where T is the signal's primary code period — that's ~10× margin from the BL · T < 0.18 stability edge of the bilinear third-order filter. The values fall out to:

SignalPrimary periodCarrier BLCode BL
GPS L1 C/A1 ms18 Hz1 Hz
GPS L5I1 ms18 Hz1 Hz
Galileo E1B4 ms4.5 Hz0.25 Hz
GPS L1C-D10 ms1.8 Hz0.1 Hz
GPS L1C-P10 ms1.8 Hz0.1 Hz

The 1-ms-primary-period signals (L1 C/A, L5I) keep the historical 18 Hz / 1 Hz default; longer-period signals get appropriately tighter loops so the PLL stays stable. Override per signal by defining methods of default_carrier_loop_filter_bandwidth / default_code_loop_filter_bandwidth, or override at construction time by passing your own doppler_estimator = to TrackState.

Doppler Estimators

Tracking.ConventionalPLLAndDLLType

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

Type parameters CA and CO select the carrier and code loop filter types; the bandwidth fields configure the loop bandwidths used when seeding new satellites. 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, so each signal gets a loop sized for its own integration period (18 Hz for GPS L1 C/A, 4.5 Hz for Galileo E1B, 1.8 Hz for L1C-D / L1C-P, …). Pass an explicit bandwidth to override the auto-sizing for every satellite this estimator seeds.

The bandwidth is referenced to a one-primary-code-period integration. When a signal coherently integrates N primary blocks (its per-TrackedSignal preferred_num_code_blocks_to_integrate, set via set_preferred_num_code_blocks_to_integrate!), the effective loop bandwidth is automatically scaled to BL/N at filter time so the loop's BL·Δt stability product stays at its single-period value. This keeps the loop stable across integration lengths without the caller re-tuning the bandwidth — e.g. a 1 ms→10 ms switch needs no bandwidth change.

source
Tracking.ConventionalAssistedPLLAndDLLFunction
ConventionalAssistedPLLAndDLL(; ...)
ConventionalAssistedPLLAndDLL(
    ;
    carrier_loop_filter_bandwidth,
    code_loop_filter_bandwidth
)

Create a ConventionalPLLAndDLL with FLL-assisted carrier tracking. This is the default Doppler estimator used by TrackState. Uses a ThirdOrderAssistedBilinearLF for the carrier loop filter which combines PLL and FLL discriminators for improved tracking under high dynamics.

Bandwidths default to nothing (auto): each satellite is seeded with the loop bandwidth recommended for its own estimator-driver signal — see ConventionalPLLAndDLL. Pass explicit bandwidths to override.

source
Tracking.default_carrier_loop_filter_bandwidthFunction
default_carrier_loop_filter_bandwidth(signal)

Recommended carrier-loop-filter bandwidth for signal's primary integration period. Sized so that the PLL time-bandwidth product BL * T lands at about 0.018 (≈10× margin from the 0.18 stability edge of the bilinear third-order filter). Used by TrackState(; signal=…) when the user doesn't pass an explicit doppler_estimator.

Override by defining a method for your signal type, or by constructing ConventionalAssistedPLLAndDLL yourself with explicit carrier_loop_filter_bandwidth = / code_loop_filter_bandwidth = kwargs.

T = get_code_length(signal) / get_code_frequency(signal)   # primary period
BL = 0.018 / T                                              # this default

T here is the primary-code period, not the chosen coherent integration length. For GPS L1 C/A (T = 1 ms) and GPS L5I (T = 1 ms, a 10230-chip code at 10.23 MHz) this returns 18 Hz — matching the historical hand-picked default. For L1C-D / L1C-P (T = 10 ms) it returns 1.8 Hz, and for Galileo E1B (T = 4 ms) 4.5 Hz — the well-inside-stability values the multi-signal flagship use case needs.

This value is the reference bandwidth for a one-primary-code-period integration; it is not the bandwidth that ends up in the loop when you integrate longer. Coherently integrating N primary blocks grows the loop update interval to N·T, which would push BL·N·T toward the ~0.18 stability edge of the bilinear filter. To avoid that, the conventional estimator automatically scales the effective loop bandwidth by 1/N at filter time (see ConventionalPLLAndDLL), holding the BL·Δt stability product fixed at its single-period value. So you set this reference bandwidth once and the loop stays stable at any integration length — no manual 1/N adjustment is needed.

source
Tracking.default_code_loop_filter_bandwidthFunction
default_code_loop_filter_bandwidth(signal)

Recommended code-loop-filter (DLL) bandwidth for signal's primary integration period. Picks 1/18 of default_carrier_loop_filter_bandwidth — the historical 18:1 carrier:code-bandwidth ratio that gives the DLL good noise rejection without lagging the PLL.

For L1 C/A and L5I (T = 1 ms) this returns 1 Hz; for L1C-D / L1C-P (T = 10 ms) it returns 0.1 Hz. Override by defining a method for your signal type.

source

Resetting loop filters

When you change a signal's coherent-integration length mid-track with set_preferred_num_code_blocks_to_integrate!, reset the affected loop filters for a clean handoff so the previous integration length's filter state does not leak into the new one.

Tracking.reset_loop_filters!Function
reset_loop_filters!(track_state)

Re-seed the Doppler-estimator state of every satellite (or one addressed satellite) from its current Doppler, giving each a freshly initialized loop filter. For the conventional PLL/DLL estimator this zeroes the carrier and code loop-filter integrators while preserving the converged carrier_doppler / code_doppler — and any per-satellite loop-bandwidth override carried on the SatConventionalPLLAndDLL state — so the loop continues from the converged frequency with a clean filter. Each signal's last_fully_integrated_filtered_prompt is cleared as well, so the first FLL update after the reset doesn't measure a prompt rotation that spans the old integration interval.

This is the recommended handoff when a signal's coherent-integration length changes mid-track — e.g. promoting GPS L5I from 1 ms to 10 ms via set_preferred_num_code_blocks_to_integrate!. The bilinear loop filter's integrator state is not portable across the change in update interval (Δt grows by the integration factor), so resetting it avoids a transient that can drag the loop out of lock; the converged Doppler is the right seed for the new, longer integration.

Addressed like the per-signal accessors — no satellite id resets every satellite in track_state; (group, prn) or (single-group) prn resets one. Mutates track_state in place and returns it. Works for any AbstractDopplerEstimator through its init_estimator_state hook.

set_preferred_num_code_blocks_to_integrate!(track_state, 1, GPSL5I, 10)
reset_loop_filters!(track_state, 1)          # clean handoff for PRN 1
reset_loop_filters!(track_state)             # …or reset every satellite
source

Custom Configuration

You can customize the loop filters and bandwidths when creating the Doppler estimator:

julia> using Tracking, GNSSSignals, TrackingLoopFilters

julia> using Tracking: Hz

julia> # Use non-assisted PLL with custom loop filter types
       doppler_estimator = ConventionalPLLAndDLL(
           ThirdOrderBilinearLF,      # carrier loop filter type
           SecondOrderBilinearLF;     # code loop filter type
           carrier_loop_filter_bandwidth = 15.0Hz,
           code_loop_filter_bandwidth = 0.5Hz
       );

julia> track_state = TrackState(; signal = GPSL1CA(), doppler_estimator);

julia> track_state = add_satellite!(track_state; prn = 1, code_phase = 50.0, carrier_doppler = 1000.0Hz);

Custom Loop Filters

You can implement a custom loop filter MyLoopFilter <: AbstractLoopFilter. In this case, a specialized filter_loop function is needed. For more information refer to TrackingLoopFilters.jl.

Custom Doppler Estimator

To replace the loop-filter-based estimator with a different algorithm (Kalman filter, joint-channel estimator, …), see the dedicated guide in custom_doppler_estimator.md.