Internals
The unexported functions and types of the package. They are listed for those extending it — e.g. adding methods of despread_noise! for a new noise source — and for those reading its source. They are not part of the public API: they may change in any release.
TrackingLoops.NavFilterModel — Type
NavFilterModelThe navigation filter's linear process model for one integration interval: F and Q, rebuilt in place whenever the measured interval changes (see ensure_nav_filter_integration_time!).
TrackingLoops.NoiseUpdateContext — Type
Per-call side information handed to update_noise!: what a software noise source may need and a Tracking.BandMeasurement does not carry.
Fields:
signal— the signal being measured, i.e. the one this estimator's key names. Its primary code period sets the sub-integration length and its code family is the one the reference despreads with, so the measurement carries that signal's own spectral weighting — which is the point of keying the estimator by signal (seeAbstractNoiseEstimator). Nothing here is chosen: it is the consumer's signal, not a per-band stand-in for it.chunk_index— the index of the chunk being measured, on the same griddownconvert_and_correlate!uses.downconvert_and_correlator— the backend running this call. A software source must despread on it rather than on a kernel of its own choosing: the one- and two-bit accumulators are popcount counts rather than sample sums, so a float-kernel reference would compare|P|²andN̂₀on incompatible scales, and the quantisation loss would drop out of the measurement instead of being carried by it.
Kept as one struct so that adding a field later is not a signature change.
Note what is not here: which PRNs are currently tracked. The reference used to need it, to skip tracked PRNs and so keep a fixed code phase 0 off their correlation peaks. CorrelatorNoiseEstimator now randomises the phase instead, so it rotates over the whole family and reads nothing from satellite state — which is what makes "open-loop" literal rather than approximate.
TrackingLoops.VectorNavigation — Type
VectorNavigationThe navigation engine of a VectorPLLAndDLL, shared by every satellite it steps: the slots, the bias layout of its signals, the navigation filter's process model, state and covariance, and every buffer a cycle needs.
It holds the latest solution (pvt, a PVTSolution whose containers every cycle reuses) and the per-member report of the latest cycle (member_sats): every member of the loop, measured and coasted, keyed exactly as pvt.sats is, each with the satellite position, transmit time and post-fit residuals that update produced. pvt.sats carries only the members the update measured, so the difference between the two key sets is what the filter predicted through an obscuration. Emptied while the scalar solve is in control.
Base.length — Method
length(estimator)
Number of observations currently in the signal's window. Diagnostic only — the window is bounded in time, so this varies with the producer's dump cadence.
TrackingLoops._advance_secondary_phase — Method
_advance_secondary_phase(
signal,
bit_buffer,
num_code_blocks
)
Walk a just-synced bit_buffer's secondary_phase forward by num_code_blocks primary-code blocks (modulo the secondary-code length). No-op for signals without a secondary code, where the field is unused.
secondary_phase is the secondary chip the upcoming integration aligns to, and it is read exactly once: by the code-phase snap (Tracking.jl's _snap_code_phase_from_synced_signal), which runs after the whole chunk has been folded. The detector reports it for the block right after the syncing record, so every further record folded in the same chunk — the ones _apply_correlator_output marks correlated_pre_sync, correlated with the pre-sync (un-wiped) replica — moves that anchor along by the blocks it covers. Without this the snap anchors the replica num_code_blocks chips early, the post-sync replica bakes the wrong overlay chip into every block, and the coherent bit sum collapses to a fraction of its length — the secondary-code sibling of the bit-grid slip in issue #219.
TrackingLoops._cfar_decide — Method
_cfar_decide(
mean_bin_energy,
bin_energy_sum_of_squared_deviations,
num_blocks,
period,
confidence
)
Shared CFAR (constant-false-alarm-rate) decision core for the soft, maximum-energy sync detectors — both the GPS L1 C/A bit-edge detector _detect_bit_edge_cfar and the secondary-code detector _detect_secondary_code_cfar route through here. Given the per-hypothesis running statistics carried in PhaseAccumulators it identifies the maximum-energy hypothesis, its closest competitor, and decides whether the peak is significant enough to lock. The two detectors differ only in what a "hypothesis" is (a bit-edge phase vs. a secondary-code rotation) and how the accumulators are fed; the decision below is identical.
Arguments:
mean_bin_energy— per-hypothesis mean completed-bin energy (accumulators.mean_bin_energy).bin_energy_sum_of_squared_deviations— per-hypothesis WelfordM₂(accumulators.bin_energy_sum_of_squared_deviations), used only for the peak.num_blocks— total primary-code blocks folded so far.period— number of primary-code blocks per bin, which is also the number of competing hypotheses:blocks_per_bit(20 for L1 C/A) or the secondary-code lengthN. A hypothesish ∈ 0:period-1has completed-bin countdiv(num_blocks - h, period).confidence— target1 − P(false lock).
Returns (accepted, peak_index, peak_bin_count): accepted is whether the peak's energy gap over its runner-up is statistically significant (the caller still applies its own bin-boundary gate before firing); peak_index is the 0-based winning hypothesis (-1 if none qualifies) and peak_bin_count its completed-bin count.
Statistic
Each hypothesis coherently sums its period-block bins and the per-bin energy |Σ|² is folded into a running mean; the true hypothesis keeps full coherent gain on every bin while wrong ones lose energy (L1 C/A: wrong phases straddle a data-bit transition; secondary code: wrong rotations fail to wipe the overlay). The per-hypothesis mean bin energy is therefore the maximum-likelihood timing statistic.
CFAR confidence
The noise scale is the bin-to-bin sample variance of the winning hypothesis's own completed-bin energies (Welford, numerically stable at any bin count). The true hypothesis's bins vary only with thermal noise and slow drift — exactly the run-to-run spread the test must compare the gap against. The peak is accepted only when it beats the runner-up by a margin significant under that spread:
z_score = energy_gap / standard_error ≥ t⁻¹(1 - false_alarm_probability/(period - 1); ν = peak_bin_count − 1)where the standard error combines the peak's per-bin energy variance over the peak and runner-up bin counts, false_alarm_probability = 1 - confidence is Bonferroni-split over the period - 1 competing hypotheses, and the quantile is the Student-t inverse-CDF _t_quantile at a nominal ν = peak_bin_count − 1 d.o.f. — a small-sample penalty for dividing by a variance estimated over that few bins, not a claim that z_score is exactly Student-t (the per-bin energies are χ² and the hypotheses correlated; see _t_quantile). A real peak has a structural gap that dwarfs the thermal bin-to-bin spread, so z_score grows like the square root of the bin count and crosses the threshold sooner at high C/N₀ and later in noise — the detector self-paces — while a drift-only asymmetry keeps z_score bounded and never locks.
TrackingLoops._detect_bit_edge_cfar — Method
_detect_bit_edge_cfar(
accumulators,
blocks_per_bit,
confidence,
num_blocks
)
Soft-decision, CFAR bit-edge detector — a signal-agnostic maximum-energy timing synchronizer for any signal whose navigation bit spans more than one primary-code period with no secondary code (selected by uses_soft_bit_edge_detection; GPS L1 C/A is the current example with blocks_per_bit = 20). Signals with a periodic secondary code (GPS L5I, GPS L1C-P) instead use the soft _detect_secondary_code_cfar or the hard _secondary_code_search, which correlate against a known overlay.
The per-phase bin statistics are carried in accumulators (PhaseAccumulators, advanced in place by _update_phase_accumulators!); blocks_per_bit is the number of primary-code blocks per navigation bit (20 for L1 C/A) and num_blocks is the total number of prompts seen. The maximum-energy hypothesis test — peak vs. runner-up, Welford noise scale, and Student-t CFAR threshold — is the shared _cfar_decide core (the hypothesis here is the bit-edge phase ∈ 0:blocks_per_bit-1). The detector is O(blocksperbit) per call — no rescan of history — so it stays cheap on the pre-sync hot path no matter how long sync takes.
Boundary firing
Detection is reported (found = true) only when the most recent block also ends the winning phase's bit (num_blocks % blocks_per_bit == peak_phase), so the upcoming integration starts a fresh navigation bit. This keeps the phase = 0 contract — and, crucially, the post-sync coherent blocks_per_bit-block integration — aligned to the true bit grid, which is what makes the off-by-one lock of issue #124 structurally impossible: the detector can only ever fire at the energy-maximizing phase's own boundary, never at a neighbour's.
polarity is the sign of the most recently completed bin's coherent sum.
TrackingLoops._detect_secondary_code_cfar — Method
_detect_secondary_code_cfar(
accumulators,
secondary_code_length,
confidence,
num_blocks
)
Soft-decision, CFAR secondary-code sync detector — the maximum-energy analog of _detect_bit_edge_cfar for signals carrying a short periodic secondary / overlay code (selected by uses_soft_secondary_code_detection — GPS L5I/L5Q, Galileo E1C / E5aI / E5aQ / E5bI / E5bQ / E6C and BeiDou B1I / B3I / B2aI / B2aQ). The per-rotation bin statistics are carried in accumulators (PhaseAccumulators, advanced in place by _update_secondary_accumulators!); secondary_code_length (N) is both the bin length and the number of rotation hypotheses, and num_blocks is the total number of prompts seen. The peak-vs-runner-up hypothesis test is the shared _cfar_decide core.
Compared with the hard-decision rotation sweep _secondary_code_search, this uses the soft prompt magnitude and a CFAR confidence, so it rejects the noise-driven false locks a short (e.g. 10-chip NH10) hard template match is prone to and self-paces with C/N₀.
Boundary firing
Detection is reported only when the most recent block also ends the winning rotation's known-code period (num_blocks % N == peak_rotation), so the upcoming integration starts at secondary chip 0 — the true chip-0 boundary, since each rotation is anchored to the physical secondary chip 0 (see _update_secondary_accumulators!). The reported SyncResult.phase is therefore always 0, and downstream code-phase snapping (Tracking.jl's _snap_code_phase_from_synced_signal) anchors on that boundary. polarity is the sign of the winning period's coherent (overlay-wiped) sum (resolved to the data-bit / carrier sign downstream by the navigation preamble).
TrackingLoops._detect_secondary_code_sync — Method
_detect_secondary_code_sync(
signal,
prn,
code_block_bits,
num_code_blocks
)
Shared detector body for signals that lock onto a periodic secondary / overlay code (GPS L5I's NH10, GPS L1C-P's 1800-chip overlay): wait until the sliding code_block_bits window covers one full secondary-code period, then run the _secondary_code_search rotation sweep against the signal's packed reference (_packed_secondary_code). The tolerance is a percentage of the secondary-code window, discretized per signal as floor(tolerance × N) — see get_bit_edge_or_secondary_code_tolerance.
A new secondary-coded signal only needs a _packed_secondary_code method (plus get_code_block_buffer_type) and a detect_bit_or_secondary_code_sync method delegating here.
TrackingLoops._detect_symbol_is_code_block_sync — Method
_detect_symbol_is_code_block_sync(_, _, _, _)
Shared detector body for signals with no sub-block boundary to find, which therefore report found = true from the very first integration.
Two shapes qualify. Most are signals that broadcast one channel symbol per primary code period (GPS L1C-D, GPS L2CM, Galileo E1B / E6-B, BeiDou B2b-I / B1C-D): the buffer of primary-block signs is itself the symbol stream, leaving downstream consumers (GNSSDecoder.jl) to resolve the residual ±1 polarity ambiguity via the navigation preamble. The other is Galileo E5a-QP, which carries neither data nor an overlay, so every block boundary is equivalent and the lock gates only its switch to the whole-code-cycle coherent integration.
A signal of either shape delegates its detect_bit_or_secondary_code_sync method here.
TrackingLoops._norm_quantile — Method
_norm_quantile(probability)
Standard-normal quantile (inverse CDF) Φ⁻¹(probability) for probability ∈ (0, 1), as √2 · erfinv(2·probability − 1) (erfinv from SpecialFunctions.jl). Used by _t_quantile as the dof → ∞ anchor that its tail expansion corrects. Returns ±Inf at probability = 1 / 0; callers keep the argument in the open interval.
TrackingLoops._packed_secondary_code — Function
Reference for _detect_secondary_code_sync: return the signal's secondary / overlay code for prn, packed into the buffer type B in the same newest-first order the prompt buffer fills — bit i holds secondary chip N - 1 - i, so that when the most recent N blocks span exactly one period ending on its last chip, received & mask == reference (see _secondary_code_search).
The single generic method below covers every signal GNSSSignals defines; it stays a function others can specialize only for a signal whose overlay is not reachable through get_secondary_code.
TrackingLoops._secondary_code_search — Method
_secondary_code_search(
received,
reference,
secondary_code_length,
max_errors
)
Generic hard-decision secondary-code sync detector for signals on the hard path — among the currently implemented signals the two 1800-chip overlay pilots, GPS L1C-P and BeiDou B1C-P, route through here. The short-secondary-code signals (GPS L5I/L5Q, the other Galileo E1/E5/E6 components, BeiDou B1I/B3I/B2a) were moved to the soft, maximum-energy _detect_secondary_code_cfar (see uses_soft_secondary_code_detection); GPS L1 C/A uses the soft bit-edge _detect_bit_edge_cfar. Unlike those, this runs a full rotation search against a known overlay code, so it locks after a single secondary-code period in the worst case and recovers the true secondary-code phase.
received is the prompt-sign sliding window with the newest block in bit 0. reference is the secondary code packed in that same newest-first order — bit i holds secondary chip (N - 1 - i) — so that when the most recent N blocks span exactly one period ending on its last chip, received & mask == reference. N is the secondary-code length.
The search rotates the low N bits of received left by d ∈ 0:N-1, tracking the best positive- and negated-polarity Hamming match in one pass. The winning rotation d is how far the buffer leads the reference, which maps to the secondary-chip offset of the upcoming integration as phase = mod(N - d, N) — exactly the value the post-sync code_phase snap (Tracking.jl's _snap_code_phase_from_synced_signal) anchors on. Returns SyncResult(false, 0, 0) when the best distance exceeds max_errors.
Inlined so the per-signal reference / N constants fold at the call site (the L1C-P window is 1800).
TrackingLoops._t_quantile — Method
_t_quantile(probability, dof)
probability-quantile of a Student-t distribution with dof degrees of freedom, i.e. t such that P(T ≤ t) = probability. Used by _detect_bit_edge_cfar in place of a standard-normal quantile as a small-sample penalty, not because the detector's z-score is exactly Student-t distributed. The z-score there divides the energy gap by a standard error built from a variance estimated over peak_bin_count bins; a normal threshold treats that estimate as exact and is badly overconfident when the count is tiny (its ~3.9 threshold is a ~10% per-phase false alarm at one d.o.f., not the intended ~1e-4), which let a variance-collapsed 1–2-bin fluctuation lock (JuliaGNSS/Tracking#124 fixed a 1-block hard-detector miss; this closes the soft detector's own few-bin false-lock). The t-quantile is the right shape of correction — it grows steeply as dof → 1 and relaxes to the normal quantile as dof → ∞ (so mature many-bin locks are unaffected, no cutoff needed) — but its nominal dof = peak_bin_count − 1 is a heuristic, not the true degrees of freedom: the per-bin values are χ² (non-central) energies rather than Gaussian, and the competing phases share blocks (correlated), so the exact sampling distribution is neither normal nor Student-t and the realised false-alarm rate is only approximately the nominal one. It is used as a conservative, integration-forcing penalty, not an exact calibration.
Implementation
Hill's algorithm (Hill, G. W. (1970), Algorithm 396: Student's t-quantiles, Comm. ACM 13(10), 619–620), driven by the two-tailed probability 2·(1 − probability): dof = 1 (Cauchy) and dof = 2 invert in elementary functions and are returned exactly, and above that a series in either the normal deviate _norm_quantile (the near-normal branch, y > 0.05 + a) or the two-tailed probability itself (the deep-tail branch, y ≤ 0.05 + a) is used, where y = (d·two_tailed)^(2/dof). The lower tail is reflected by symmetry, and the median t(0.5) = 0 is returned directly — which also avoids a 1 − 0.5 self-recursion. The sole caller passes probability > 0.5 (and dof ≥ 1, since peak_bin_count ≥ 2 at the call site).
Hill's series is accurate to ≲2e-4 relative — worst around dof ≈ 3–10, better than 1e-5 by dof ≈ 50 — which is orders of magnitude finer than the nominal-d.o.f. modelling error above, and far too fine to move which block the detector locks on. That error buys both of the properties this call site needs, against an exact inversion through SpecialFunctions.beta_inc_inv:
- Allocation-free.
beta_inc_invallocates internal scratch arrays (zeros(22)/zeros(31)inside the incomplete-beta asymptotic expansions) over part of thedofrange the detector sweeps. This function runs once per primary-code block for every unsynced satellite, so an allocation here makestrack!allocate in proportion to the signal length across the whole acquisition — the regressiontest/track_in_place.jl's pre-sync guard exists to catch. - Cheap.
beta_inc_invis an iterative Newton solve that evaluates a full regularized incomplete beta per step (~0.2–2.8 µs here); Hill's is a single closed-form pass (~3–78 ns), 10–170× faster over the wholedofrange and never slower. End to end that is a pre-synctrack!at 1.7× (200 blocks) to 1.3× (900 blocks).
TrackingLoops._update_phase_accumulators! — Method
_update_phase_accumulators!(
accumulators,
prompt,
block_index,
blocks_per_bit
)
Fold the prompt of the primary-code block at 0-based block_index into the PhaseAccumulators (in place) for a blocks_per_bit-phase bit-edge search. Each phase's bins start at index phase and span blocks_per_bit blocks; prompt is added to every phase whose bin is open at block_index (block_index ≥ phase), and the single phase whose bin ends at block_index has that completed bin's energy |bin_sum|² folded into its Welford mean / sum-of-squared-deviations and its polarity recorded. O(blocksperbit) per call, allocation-free.
TrackingLoops._update_secondary_accumulators! — Method
_update_secondary_accumulators!(
accumulators,
prompt,
block_index,
secondary_code_length,
signal,
prn
)
Fold the prompt of the primary-code block at 0-based block_index into the PhaseAccumulators (in place) for a soft, maximum-energy secondary-code rotation search of period N = secondary_code_length. This is the secondary-code analog of _update_phase_accumulators!: each rotation hypothesis d ∈ 0:N-1 is a candidate secondary-chip alignment whose bins span N blocks starting at index d. Unlike the bit-edge case, each block is multiplied by the known secondary chip before being summed, so the correct rotation wipes the overlay and every block in the bin adds coherently (|Σ p·s|² large) while wrong rotations straddle the fixed overlay transitions and lose energy.
signal / prn select the secondary code via secondary_value; under hypothesis d, block i carries secondary chip mod(i - d, N) (±1), so the bin completes when chip == N - 1 and chip 0 is anchored to the physical secondary-code chip 0 — the same overlay the replica applies post-sync, and the same rotation the hard _secondary_code_search locks (its bespoke packed reference differs only by an overall polarity, which the energy statistic here is invariant to). Each completed bin's energy |bin_sum|² is folded into that rotation's Welford mean / sum-of-squared-deviations and its polarity recorded. O(N) per call, allocation-free.
TrackingLoops.buffer — Method
buffer(
signal,
prn,
bit_buffer,
integrated_code_blocks,
prompt
)
Buffer data bits based on the prompt accumulation and the current prompt value.
Post-sync, integrated_code_blocks is added to a running count and one soft bit is emitted each time that count reaches the signal's blocks-per-bit. A record that carries the count past the boundary — which only an external CorrelatorOutput producer whose records are not bit-aligned can produce — drops bit sync instead (found returns to false, so the detector re-runs from a clean accumulator); see issue #238. It does not log: apply_record reports the drop as its overshoot flag, and the caller decides what to do with it.
TrackingLoops.despread_noise! — Function
despread_noise!(backend, estimator, measurement, first_sample, last_sample, context)The software fill path of a CorrelatorNoiseEstimator: despread an untracked PRN over the slice with backend's own kernel and append the observations. Implemented by Tracking.jl for its downconvert-and-correlate backends; declared here so the estimator type can live without them.
TrackingLoops.fold_record — Method
fold_record(signal, prn, bit_buffer, cn0_estimator, post_corr_filter, output,
sampling_frequency, noise_density, noise_density_ready,
driver_carrier_phase, correlated_pre_sync)
-> (bit_buffer, cn0_estimator, post_corr_filter, prompt, filtered_correlator,
bit_block_count, integrated_code_blocks, overshoot)Apply one completed record to the bare per-record state of a signal component: normalize the record's raw correlator by its sample count and code amplitude, update and apply the post-correlation filter, advance the C/N₀ estimator and the bit buffer. Returns the new state values plus the filtered correlator and block counts the loop-filter step needs.
The bit accumulator is credited with the blocks actually integrated (calc_num_code_blocks_for_bit_buffer), recovered from the record's sample count. correlated_pre_sync = true marks a record that follows a bit/secondary sync detected earlier in the same fold: its prompt is kept out of the coherent bit accumulation for secondary-coded signals (the sync changed the replica under it) but its blocks are always credited, and it moves the secondary-code anchor along. overshoot reports a post-sync record that carried the bit accumulator past the navigation-bit boundary, on which the bit buffer dropped sync and restarted its search (issue #238). Everything here is the arithmetic Tracking's per-record advance performs, in the same order.
TrackingLoops.is_zero — Method
is_zero(correlator)
Is zero correlator