C/N₀ estimation
A C/N₀ estimator is advanced once per record with the record's prompt (TrackingLoops.update, called by apply_record) and read with estimate_cn0. The package provides:
MomentsCN0Estimator— the moments method on a window of prompts;NWPRCN0Estimator— the narrow-to-wideband power ratio, summing prompts coherently over one navigation symbol once the bit grid is known;NoiseRefCN0Estimator— divides the prompt power by a noise density measured independently of the signal (see Noise estimation);NoCN0Estimator— for a component whose C/N₀ is not needed.
default_cn0_estimator says which one a signal gets by default. A custom estimator subtypes AbstractCN0Estimator and implements TrackingLoops.update and estimate_cn0, plus requires_noise_density if it reads a measured noise floor.
TrackingLoops.AbstractCN0Estimator — Type
Abstract supertype for CN0 (carrier-to-noise-density ratio) estimators. Each Tracking.TrackedSignal holds one estimator instance, stored in a type parameter — pass any subtype instance as the cn0_estimator keyword of Tracking.TrackedSignal / Tracking.TrackedSat to replace the default NoiseRefCN0Estimator; see default_cn0_estimator for which to pick when. Custom estimators subtype this and implement TrackingLoops.update and estimate_cn0, plus requires_noise_density if they read a measured noise floor.
TrackingLoops.CN0UpdateContext — Type
Per-record side information handed to TrackingLoops.update alongside the prompt: everything the tracking loop knows about the record that an estimator cannot recover from the prompt stream alone. Its reason for existing is the navigation-bit grid — where the data-bit boundaries sit and whether they are known yet — which is what lets an estimator sum prompts coherently over exactly one symbol (NWPRCN0Estimator does) instead of straddling a bit flip.
Fields:
signal— the signal the record belongs to.num_code_blocks— primary-code blocks this record spanned. One for the default configuration; more when the correlate step was lengthened byTracking.set_preferred_num_code_blocks_to_integrate!or an external producer handed over longer records.num_code_blocks_per_bit— blocks that form one navigation bit (symbol) ofsignal;20for GPS L1 C/A,1for GPS L1C-D / Galileo E1B, and0for a pilot, which carries no data and therefore has no bit grid — post-sync its prompts stay coherent for as long as the loops hold them.bit_code_block_index— blocks already accumulated into the currently open navigation bit before this record, i.e. this record's own offset inside the bit;0means the record starts a fresh bit. It is-1whenever this record's prompt cannot be summed coherently against the grid: before the signal's bit / secondary-code sync has been found, and — for a secondary-coded signal only — for a record of the fold that follows a sync detected in that same fold, which was correlated without the overlay wipe-off the sync just established (see_apply_correlator_output). Such a record's blocks still count towards the grid, so the offset stays right for the records after it.bit_buffer— the signal'sBitBufferas of before this record: the soft bits decoded so far, the open coherent bit accumulator, the lock polarity and the secondary-code phase.noise_density— this signal's measured noise densityN₀(dimension1/Hz), for an estimator that divides the prompt's power by a measured floor instead of inferring one from the prompt stream (NoiseRefCN0Estimatordoes). Per signal and not per band because the floor is the post-correlation one — seeAbstractNoiseEstimator. It is a type parameter, not a sentinel:Nothingsays noAbstractNoiseEstimatoris configured for this signal at all, which is a static property of the setup, so every call site monomorphises and the estimators stay allocation-free. A configured source whose window is merely still empty never reaches here — the fold skips the update instead, so this field is unconditionally a plain scalar whenever it is notNothing.integration_time— this record's ownT, so an estimator that works per record does not have to be told oneTfor a whole ring of records integrated at different lengths.nothingwhen the caller did not supply one (the bare-prompt-stream path).
TrackingLoops.default_cn0_estimator — Method
default_cn0_estimator(
signal,
num_prompts_for_cn0_estimation
)
The default CN0 estimator for signal: a NoiseRefCN0Estimator averaging over num_prompts_for_cn0_estimation records, against that signal's own measured noise density.
It reads a density, so Tracking.TrackState provisions the signal a CorrelatorNoiseEstimator automatically (see requires_noise_density) and track! fills it from the samples — the sample-driven path needs no configuration at all.
Why this and not NWPR
NWPRCN0Estimator is accurate where it applies, but it needs a coherent narrowband window and so does not apply uniformly. Measured through track! on a data-modulated GPS L1 C/A signal, 1200 code blocks, median over 9 seeds, with the fraction of runs reporting -Inf dB-Hz in brackets:
| true | NWPR, 1-block records | NoiseRef | NWPR, 20-block records | NoiseRef |
|---|---|---|---|---|
| 25 | 11.2 (56 %) | 23.4 (0) | — | — |
| 30 | 29.3 ± 1.40 | 29.6 ± 0.62 | — | — |
| 40 | 39.7 ± 0.35 | 39.9 ± 0.19 | 27.1 ± 5.79 | 39.8 ± 0.37 |
| 45 | 44.7 ± 0.30 | 44.9 ± 0.13 | 32.9 ± 2.00 | 44.7 ± 0.30 |
Three things in that table decided the default. At 25 dB-Hz NWPR's window lands outside 1 < μ̂ < M in over half the runs and the surviving estimates read 14 dB low, which is exactly the regime a lock detector has to work in. At long coherent records NWPR collapses — a record as long as its own window has NBP ≡ WBP and no window exists at all, so it falls back — while the non-coherent reference is immune to the phase-noise wash and barely moves between 1- and 20-block records. And on GPS L1C-D, Galileo E1B and any secondary-coded signal before sync, NWPR admits no window ever and defers permanently to a fallback with a different bias (issue #217).
What NWPR is still better at is the top of the range, where the reference carries a self-leakage bias it does not: ≈0.13 dB at 45 dB-Hz and ≈0.40 at 50 on L1 C/A. See NoiseRefCN0Estimator.
When to pass something else
NWPRCN0Estimator remains exported and is the estimator to configure explicitly for externally supplied correlator outputs without a noise observation — a correlator-ingest path that cannot also report Σ|B|² for an untracked PRN. It is the one place it is still necessary; everywhere else, prefer appending a NoiseObservation per signal with append_noise_observation!.
TrackingLoops.requires_noise_density — Method
requires_noise_density(_)
Does estimator read its signal's noise density out of its CN0UpdateContext? false for every estimator that infers its noise floor from the prompt stream (MomentsCN0Estimator, NWPRCN0Estimator, NoCN0Estimator); true only for NoiseRefCN0Estimator.
It is a trait rather than a hard-coded type check because update(::AbstractCN0Estimator, ::Any, ::CN0UpdateContext) is a documented extension point: a third-party estimator that wants a density must be able to say so, and one that does not must not pay for it.
Two things key off it, and both are compile-time constants on the estimator's type:
- Provisioning.
Tracking.TrackStategives a signal aCorrelatorNoiseEstimatoronly where that signal's estimator returnstrue. A signal that does not ask gets no entry at all, so its despread never runs and costs exactly zero — which is the answer for anyone who deliberately stays on NWPR. - The warm-up skip. While a configured source's window is still empty, the fold skips the C/N₀ update for the requiring signal only — its co-residents on the same band and the same satellite are untouched, because each has its own source. An
NWPRCN0Estimatorbeside it would otherwise be corrupted: a record missing from the bit grid makes_update_nwprdrop its open narrowband window, so NWPR would silently degrade to its fallback.
The trait's real home is the type, and the instance method forwards to it. That is what lets provisioning be decided from a group's already-fixed slot type rather than from a satellite value: the whole noise_estimators NamedTuple — its keys included — then folds out of TrackState's type parameters instead of inferring as a union of "provisioned" and "not". A custom estimator may define either form; defining the type form is the one that keeps that folding.
TrackingLoops.update — Method
update(estimator, prompt, _)
Fold one completed record's prompt into estimator and return the updated estimator (immutable update). This three-argument form is the one the tracking loop calls and the extension point for custom AbstractCN0Estimators that profit from the navigation-bit information in context (see CN0UpdateContext).
The default implementation drops context and calls the two-argument TrackingLoops.update(estimator, prompt), so an estimator that only needs the prompt stream — like MomentsCN0Estimator — implements just that one.
TrackingLoops.MomentsCN0Estimator — Type
MomentsCN0Estimator to estimate the CN0
TrackingLoops.estimate_cn0 — Method
estimate_cn0(estimator, integration_time)
Estimates the CN0 based on the struct MomentsCN0Estimator.
TrackingLoops.update — Method
update(estimator, prompt)
Buffers the prompts such that they can be used to estimate the CN0. Returns a new estimator with the latest prompt added (immutable update). The moment ratio needs no side information, so this estimator implements only the prompt-only form of TrackingLoops.update; see update(::AbstractCN0Estimator, ::Any, ::CN0UpdateContext) for the extension point that also receives the navigation-bit context.
TrackingLoops.NWPRCN0Estimator — Type
Van Dierendonck's narrowband/wideband power ratio (NWPR) CN0 estimator.
It was the library default until the per-band noise reference landed, and remains the estimator to configure explicitly on a correlator-ingest path with no noise observation — a producer that hands over CorrelatorOutputs but cannot also report Σ|B|² for an untracked PRN. That is the one place it is still the right choice; see default_cn0_estimator for the measurements that moved the default, and NWPRCN0Estimator(::AbstractGNSSSignal) for the constructor that sizes its window for the signal.
Over a narrowband window of M consecutive records it forms
NBP = |Σ_M prompt|² (coherent — narrowband)
WBP = Σ_M |prompt|² (incoherent — wideband)combines the windows that fit in num_records records into a mean power ratio μ̂, and reports
μ̂ = Σ_K NBP_k / Σ_K WBP_k
Ĉ/N₀ = (1 / T) · (μ̂ − 1) / (M − μ̂)with T the record's own integration time (the integration_time argument of estimate_cn0). Reference: A. J. Van Dierendonck, "GPS Receivers", ch. 8 in Global Positioning System: Theory and Applications, Vol. I, ed. B. W. Parkinson & J. J. Spilker Jr.; the formulas above are reproduced on ESA Navipedia.
Why the ratio of the sums, and not the mean of the ratios
The reference spells the mean ratio as μ̂ = (1/K) Σ NBP_k / WBP_k, the mean of the per-window ratios. The inversion (μ̂ − 1) / (M − μ̂) is derived from μ = E[NBP] / E[WBP], which the ratio of the sums estimates consistently and the mean of the ratios does not: E[NBP/WBP] < E[NBP]/E[WBP] at finite M, so the mean of the ratios reads low. Measured on synthetic prompts with K → ∞, so that only the bias is left:
| true C/N₀ | mean of ratios, M = 2 | M = 5 | M = 20 | ratio of sums, any M |
|---|---|---|---|---|
| 20 dB-Hz | 18.4 | 19.3 | 19.8 | 20.0 |
| 25 dB-Hz | 23.6 | 24.4 | 24.9 | 25.0 |
| 30 dB-Hz | 28.9 | 29.6 | 29.9 | 30.0 |
The size of the correction is a function of the window length, and at the default five-block window it is small: ~0.6 dB of the bias above is left there, and in the loop the two forms agree to a few tenths (+0.2 / −0.1 / +0.4 dB at a true 20 / 25 / 30 dB-Hz, measured from the same buffered windows). It earns its keep at shorter windows — +1.4 / +1.9 / +1.0 dB at a two-block one — and at the M = 20 of the classic GPS L1 C/A configuration the two agree to ~0.15 dB, which is why the literature's form does no harm there. The reason to prefer the ratio of the sums anyway is that it is the consistent estimator of μ at every window length, so the estimate does not depend on how the window was chosen.
The correction is a shift, not a variance trade: the spread is unchanged, and at equal false-alarm rate the ratio of the sums is the slightly better detector too — at M = 2 and a 0.1 % false-alarm rate it detects a true 25 dB-Hz signal in 22.5 % of updates against the mean of the ratios' 15.0 %.
Why it is the default
NWPR is usable as a detection statistic near threshold, which MomentsCN0Estimator is not: the moment ratio's sample moments manufacture signal power out of noise at finite window length, so at its default 100-prompt window M2M4 reports a median of ~27.6 dB-Hz on pure noise and cannot separate a true 20 dB-Hz signal from noise at all (a code lock threshold below ~30 dB-Hz can never trip, and one at 30 dB-Hz has a ~19 % per-update false-alarm rate). NWPR on the identical prompt streams tracks the truth from 20 dB-Hz up and reports "no signal" on noise, with a visibly tighter spread below 32 dB-Hz (Falletti, Pini & Lo Presti, IEEE T-AES 47(1):420–437, 2011). See JuliaGNSS/Tracking.jl#217 for the measurements.
The coherence constraint, and why this belongs in Tracking
NBP is a coherent sum, so a window must not straddle a navigation-bit flip (a mid-window flip costs ~7 dB) and must stay short against the residual Doppler (M·T ≪ 1/(2·Δf); 25 Hz over a 20 ms window is half a cycle and costs ~9 dB). Where the bit flips sit is something the tracking loop knows and a consumer of get_filtered_prompts does not, so the window is taken from the navigation-bit grid in CN0UpdateContext — that is the whole reason this estimator lives here rather than on top of the prompt stream:
| signal state | narrowband window |
|---|---|
| bit / secondary sync found, data-bearing signal | num_narrowband_code_blocks, tiling the navigation bit from its start |
| bit / secondary sync found, pilot (no data) | num_narrowband_code_blocks (no bit grid to respect) |
| sync not found yet, data-bearing without secondary code | num_presync_narrowband_code_blocks, unaligned |
| sync not found yet, signal with a secondary code | none — the unknown overlay flips sign every code block |
| one symbol per code block (GPS L1C-D, Galileo E1B) | none — no coherent window longer than one record exists |
| record at least as long as its own window | none — a one-record window has NBP == WBP by construction |
The pre-sync window matters more than it may look: the CFAR bit-edge detector needs seconds to lock at 35 dB-Hz and does not lock at all below ~30 dB-Hz, so a bit-aligned window alone would leave exactly the regime this estimator exists for on the fallback estimator. An unaligned window of M records inside an L-block bit straddles a flip with probability (M−1)/L, which at the default five blocks of a 20-block GPS L1 C/A bit costs ~0.6 dB of bias below 30 dB-Hz (and up to ~6 dB at a strong signal, for the fraction of a second until sync is found) while removing the moment ratio's noise floor entirely.
The window is capped by the loop's coherence time, not by the bit period
A bit-aligned window does not have to span the whole bit: windows of num_narrowband_code_blocks tile the bit from its start, which straddles no flip either and keeps the coherent sum inside the loop's coherence time. That cap is what the length is for — the longest flip-free window is not the best one. The gain from a longer window saturates quickly, because num_records records are combined either way and only their partition changes: at a true 25 dB-Hz, going from a 5-record to a 20-record window buys 0.8 dB of spread. The cost does not saturate. Residual phase noise makes the coherent sum lose power, which is a bias — the one error more averaging cannot remove. In the loop at a true 25 dB-Hz, with the conventional PLL at 1 ms records:
| window | 2 records | 5 | 10 | 20 (one full L1 C/A bit) |
|---|---|---|---|---|
| reported C/N₀ | 24.6 | 22.9 | 19.8 | 15.9 |
so the default cap is deliberately short (~5 ms of code blocks, see default_cn0_estimator). Raise it for a pilot, a narrow carrier loop or a signal that is never weak; the ideal-coherence estimate improves monotonically with it.
Where no window is admissible at all the fallback estimator's value is reported instead — a different estimator, with a different bias and, at the default MomentsCN0Estimator, a ≈27.6 dB-Hz floor on pure noise. That is what NoiseRefCN0Estimator exists to retire (issue #217). It also includes records that are themselves at least as long as the window — with Tracking.set_preferred_num_code_blocks_to_integrate! at one navigation bit, say, a window closes on a single record, NBP == WBP identically, and the estimator reports its fallback for good.
Fields / configuration
num_records— how many records the estimate averages over, i.e. the memory of the estimator (100 by default, ~100 ms at GPS L1 C/A). The ring buffer of ratios is sized from it:num_records ÷ Mratios are averaged, so the memory stays put whenMchanges.num_narrowband_code_blocks— window length in primary-code blocks: the cap on the coherent sum, and the window length itself for a signal with no navigation-bit grid (a pilot, or a bare prompt stream fed through the two-argumentTrackingLoops.update).num_presync_narrowband_code_blocks— window length in primary-code blocks used while the bit grid is still unknown (see the table above);0disables the pre-sync window and reports thefallbackuntil sync.buffered_narrowband_powers,buffered_wideband_powers,ratio_current_index,filled_ratio_length,num_records_per_ratio,ratios_are_bit_aligned— the ring buffers of completed windows'NBPandWBP, the record countMthey were formed with, and whether they followed the navigation-bit grid. The two powers are buffered separately because the estimate divides their sums.Menters the estimate, so a window completing with a different record count (records lengthened at bit sync, say) restarts the buffers — and so does a window completing on the other side of the sync transition, since a pre-sync window may have straddled a bit flip and must not be averaged together with clean bit-aligned ones. A window closing on a single record empties them instead of restarting them: the records have grown to the window length, so there is nothing left to average and thefallbacktakes over (the last row of the table above).narrowband_sum,wideband_power,num_accumulated_records,num_accumulated_code_blocks— the currently open window.fallback— the estimator reported while no window has completed yet, and for the signals of the table above that never get one. Defaults to aMomentsCN0Estimatorand is fed every prompt.
TrackingLoops.NWPRCN0Estimator — Method
NWPRCN0Estimator(
signal;
num_records,
num_narrowband_code_blocks,
kwargs...
)
Construct an NWPRCN0Estimator whose coherent window is sized for signal: the whole code blocks covering about 5 ms, at least two of them — 5 blocks for a 1 ms code, 2 for GPS L1C-P's 10 ms one.
About 5 ms is what a coherent sum survives with the default 18 Hz carrier loop at the low C/N₀ this estimator exists for; see NWPRCN0Estimator for the measurements and for when to raise it. The window is what caps the coherent sum for a data-bearing signal (whose windows tile the navigation bit) and is the window outright for a pilot, so sizing it in blocks without knowing the code period gets it wrong by the period's ratio.
This is the form to reach for on a correlator-ingest path with no noise observation, which is the one place NWPR is still the estimator to choose over the default NoiseRefCN0Estimator — see default_cn0_estimator:
TrackedSat(
GPSL1C_P(),
prn,
code_phase,
doppler;
cn0_estimator = NWPRCN0Estimator(GPSL1C_P()),
)Every other keyword is forwarded unchanged.
TrackingLoops.NWPRCN0Estimator — Method
NWPRCN0Estimator(
;
num_records,
num_narrowband_code_blocks,
num_presync_narrowband_code_blocks,
fallback
)
Construct a fresh NWPRCN0Estimator averaging over the last num_records records. num_narrowband_code_blocks caps the coherent window (and is the window length outright where there is no navigation-bit period to tile), num_presync_narrowband_code_blocks is the window length used while the bit grid is unknown — see NWPRCN0Estimator for the full table and for what the defaults cost.
fallback is the estimator reported until the first window completes; it defaults to a MomentsCN0Estimator over the same num_records prompts.
TrackingLoops.estimate_cn0 — Method
estimate_cn0(estimator, integration_time)
Estimate the CN0 from the buffered narrowband and wideband powers, dividing by integration_time — the record's integration time, which is the predetection integration time T of Van Dierendonck's formula (see NWPRCN0Estimator).
The mean power ratio is μ̂ = Σ NBP_k / Σ WBP_k — the ratio of the sums, not the mean of the per-window ratios, which is biased low at short windows; see NWPRCN0Estimator.
Until the first narrowband window has completed — and for good on a signal that admits no window at all — the fallback estimator's value is returned instead.
The mean ratio μ̂ lies in [1, M] by construction and both ends are reported as the limits of the expression rather than clamped to a magic number: μ̂ ≤ 1 means the coherent sum holds no more power than the incoherent one, i.e. no detectable signal, and yields -Inf dB-Hz; μ̂ ≥ M means no detectable noise and yields Inf dB-Hz (the same value a noise-free signal gets out of MomentsCN0Estimator). A consumer thresholding the estimate needs no special case for either; one averaging it does, which is why the infinities are documented rather than hidden behind an epsilon.
TrackingLoops.update — Method
update(estimator, prompt, context)
Accumulate one record's prompt into the open narrowband window, taking the window length and its alignment from the navigation-bit grid in context (see NWPRCN0Estimator for the per-signal-state table). A window that follows the bit grid is only started where a window may start — the bit's own boundary, or a multiple of the window length inside the bit — and an open window is dropped rather than closed whenever it is no longer admissible: no window at all applies to this record (sync lost, or never established), or the grid moved under it when sync was found.
A closed window contributes to the estimate only if it held at least two records; with one record NBP == WBP identically and the window carries no information.
TrackingLoops.update — Method
update(estimator, prompt)
Advance the estimator on a bare prompt stream, with no navigation-bit context: each prompt counts as a one-block record and windows run back to back at num_narrowband_code_blocks, aligned to the first prompt. This is the form to use when folding a captured prompt stream by hand; inside track the three-argument form above is called, which aligns the window to the navigation-bit grid instead.
TrackingLoops.NoiseRefCN0Estimator — Type
C/N₀ against a measured noise reference: per record,
Ĉ/N₀ = ⟨|P|²⟩ / N̂₀ − 1/Twith N̂₀ the signal's noise density from its AbstractNoiseEstimator and T that record's own integration time. The ring averages the per-record terms and estimate_cn0 converts the mean once.
Unlike NWPRCN0Estimator this is non-coherent: it needs no bit sync, no window length M, no coherent window and no fallback, it is immune to residual carrier-phase error, and it has no saturation ceiling. It is therefore the one estimator that works uniformly on every signal — including GPS L1C-D and Galileo E1B (blocks_per_bit == 1) and any secondary-coded signal before sync, where NWPR admits no coherent window at all and defers permanently to a fallback with a different bias (issue #217).
What it needs
A noise density for its own signal — not for its RF band, because what this divides by is the post-correlation floor and that depends on the despreading modulation (see AbstractNoiseEstimator). On the sample-driven path it is automatic: Tracking.TrackState provisions a CorrelatorNoiseEstimator for every signal whose estimator asks for one (see requires_noise_density), and track! measures before the fold reads. On a correlator-ingest path you configure the same type and fill it with append_noise_observation! per signal instead. With no source configured for the signal, update throws — a wiring mistake, and a silent substitution would hide a backend that fails to populate the reference. While a configured source's window is merely still empty, the update is skipped (the fold warns once per signal) and estimate_cn0 reports -Inf dB-Hz until it fills.
Three deliberate properties
- No
fallbackfield. The three reasons NWPR needs one — warm-up, no admissible window pre-sync, and no window ever at one symbol per code block — all vanish. One record plus a density is a valid, if noisy, estimate on every signal. estimate_cn0'sintegration_timeargument is ignored, becauseTis applied per record at update time, where it is actually known. The signature stays for interface uniformity; this is not an oversight.- Only the average is floored, never the per-record term. Individual terms go negative at low C/N₀ and that is exactly what makes the mean unbiased; clamping per record would reintroduce a noise floor of the kind
MomentsCN0Estimatorhas.
The one bias it carries
The reference despreads with a wrong PRN, so besides the thermal floor and the other satellites' interference — both of which NWPR sees identically — it also collects the tracked satellite's own power, ε_self/N₀ = C/f_chip. NWPR does not: it despreads with the correct code, where the satellite's power appears in both NBP and WBP and cancels in the ratio. On GPS L1 C/A that is 0.013 dB at 35 dB-Hz, 0.042 at 40, 0.13 at 45 and 0.40 at 50 (≈10× smaller on the 10.23 Mcps signals), always reading low. It is carried rather than corrected, because the correction N̂₀ ← N̂₀ − Σᵢ Ĉᵢ/f_chip would make every satellite's C/N₀ a function of every other satellite's estimate — the cross-satellite feedback loop this design exists without. Below 40 dB-Hz it sits under NWPR's own +0.05 dB bias, and that is the range where lock and loss decisions are made.
Fields / configuration
num_records— how many records the estimate averages over, i.e. the memory of the estimator (100 by default, ~100 ms at GPS L1 C/A).buffered_cn0,current_index,filled_length— the ring of per-record C/N₀ terms in linear Hz, written in place, plus its position and fill.
TrackingLoops.NoiseRefCN0Estimator — Method
NoiseRefCN0Estimator(; num_records)
Construct a fresh NoiseRefCN0Estimator averaging over the last num_records records.
TrackingLoops.estimate_cn0 — Method
estimate_cn0(estimator, integration_time)
Mean of the buffered per-record terms, converted once with dBHz.
integration_time is ignored: T was applied per record in update, where each record's own value was known — a record lengthened by Tracking.set_preferred_num_code_blocks_to_integrate! is therefore handled correctly even when it sits in the ring beside shorter ones. The argument stays for interface uniformity with the other estimators.
An empty ring, a mean that has not cleared zero, and a non-finite mean all report -Inf dB-Hz — the house convention that a missing estimate is -Inf and never NaN (see NoCN0Estimator for why). The last case is what makes the convention hold rather than merely be intended: NaN dB-Hz compares >= true against every lock threshold, so letting one out would turn a dead signal into a locked one, and mean_cn0 <= 0 is exactly the test a NaN passes through. _noise_density_and_ready keeps the reachable source of one — a zero measured floor — out of the ring in the first place; this covers what a NaN prompt would still put there.
TrackingLoops.requires_noise_density — Method
requires_noise_density(_)
This estimator reads its signal's noise density, so a signal carrying it is provisioned with a CorrelatorNoiseEstimator. Declared on the type, so the provisioning decision folds out of a group's slot type — see requires_noise_density.
TrackingLoops.update — Method
update(estimator, prompt, context)
Fold one record's prompt into the ring: |P|²/N̂₀ − 1/T in linear Hz, with the density and the record's own integration time taken from context.
The term is not clamped — at low C/N₀ individual terms are negative, and that is what keeps the mean unbiased.
TrackingLoops.update — Method
update(_, prompt)
A bare prompt stream carries no noise density and no integration time, so this estimator has no two-argument form — use the three-argument update(::AbstractCN0Estimator, ::Any, ::CN0UpdateContext), which is what the tracking loop calls, or MomentsCN0Estimator / NWPRCN0Estimator for folding a captured prompt stream by hand.
TrackingLoops.NoCN0Estimator — Type
A CN0 estimator that estimates nothing: it keeps no state, does no work per record, and reports -Inf dB-Hz. Use it to say "do not measure this signal's C/N₀" — either to skip the per-record work on a signal whose C/N₀ nobody reads, or, more importantly, to avoid publishing a number that cannot be trusted.
Two places where that matters:
- the non-driver signals of a multi-signal
Tracking.TrackedSatwhose C/N₀ is never read:cn0_estimator = (NWPRCN0Estimator(), NoCN0Estimator())(the saving is small — the per-record update is well under a per cent of a correlation — so reach for this for the reason below, not for speed); - as
NWPRCN0Estimator'sfallback, where it replaces theMomentsCN0Estimator's ~27.6 dB-Hz noise floor with an honest "no estimate" for the signals and phases that admit no coherent window (a secondary-coded signal before sync, or GPS L1C-D / Galileo E1B at any time):NWPRCN0Estimator(; fallback = NoCN0Estimator()).
-Inf dB-Hz rather than NaN dB-Hz, even though "not measured" is what is meant: NaN dB-Hz >= threshold is true for every threshold with Unitful's Level comparison, so a NaN would clear every lock detector it met. -Inf compares false against any finite threshold, which is the safe answer to "is this signal locked?".
TrackingLoops.estimate_cn0 — Method
estimate_cn0(_, integration_time)
Always -Inf dB-Hz; see NoCN0Estimator for why that value and not NaN.
TrackingLoops.update — Method
update(estimator, prompt)
Ignore the prompt and return the estimator unchanged — NoCN0Estimator holds no state.