API Reference
Acquisition Functions
Acquisition.acquire — Function
acquire(system, signal, sampling_freq, prns; kwargs...) -> Vector{AcquisitionResults}Convenience wrapper: calls plan_acquire then acquire!.
Arguments
system: GNSS system (e.g.GPSL1CA())signal: Complex baseband signal samplessampling_freq: Sampling frequencyprns: PRN numbers to search
Keyword Arguments forwarded to plan_acquire:
min_doppler_coverage: Minimum one-sided Doppler coverage (default:7000Hz)num_coherently_integrated_code_periods: Code periods per coherent block (default:1)bit_edge_search_steps: Bit edge search positions (default:1)num_noncoherent_accumulations: Non-coherent integration steps (default:1)use_secondary_code: enable the secondary-code rotation search (default:true). Requiresnum_coherently_integrated_code_periodsto be a whole multiple of the secondary-code lengthL; a partial period is rejected to avoid the ±Doppler sign ambiguity (issue #68). Seeplan_acquire.fft_flag: FFTW planning flag (default:FFTW.MEASURE)
Keyword Arguments forwarded to acquire!:
interm_freq: Intermediate frequency (default:0.0Hz)subsample_interpolation: Enable parabolic interpolation (default:false)store_power_bins: Retain the full correlation power surface in each result for plotting (default:false)
Returns
Vector{AcquisitionResults}, one per PRN.
See also
acquire(system, signal, sampling_freq, prn::Integer; kwargs...) -> AcquisitionResultsSingle-PRN convenience method. Returns a single AcquisitionResults.
Acquisition.acquire! — Function
acquire!(plan::AcquisitionPlan, signal, prns; interm_freq=0.0Hz, subsample_interpolation=false, store_power_bins=false) -> Vector{AcquisitionResults}Perform FM-DBZP acquisition using a pre-computed AcquisitionPlan.
Reuses all pre-allocated buffers in plan. Multiple PRNs are processed in a single pass.
Arguments
plan: Pre-computedAcquisitionPlan(fromplan_acquire)signal: Complex baseband signal samplesprns: PRN numbers to search (must be a subset ofplan.avail_prns)
Keyword Arguments
interm_freq: Intermediate frequency (default:0.0Hz)subsample_interpolation: Whentrue, apply parabolic interpolation to refine the code phase and Doppler estimates below the grid resolution (default:false)store_power_bins: Whentrue, copy the full Doppler × code-phase correlation power matrix into each result'spower_binsfield (required for plotting). Whenfalse,power_binsisnothingand no extra copy is made (default:false)
Returns
Vector{AcquisitionResults}, one entry per PRN in prns.
See also
acquire!(plan::AcquisitionPlan, signal, prn::Integer; kwargs...) -> AcquisitionResultsSingle-PRN convenience method. Calls the multi-PRN acquire! and returns the single result.
Acquisition.plan_acquire — Function
plan_acquire(system, sampling_freq, prns; kwargs...) -> AcquisitionPlanPre-compute an acquisition plan for FM-DBZP acquisition.
Allocates all buffers, computes FFT plans, and pre-computes conjugated PRN FFTs. The returned plan can be passed to acquire! repeatedly without re-allocating. The per-thread scratch pool is sized to min(Threads.nthreads(), num_cores) at the time of the call; start Julia with -t N before calling plan_acquire to enable multi-threaded acquisition. Scratch buffers are tile-sized (nothing scales with samples_per_code), so extra threads cost little memory.
Arguments
system: GNSS system (e.g.GPSL1CA())sampling_freq: Sampling frequencyprns: PRN numbers to pre-compute (e.g.1:32)
Keyword Arguments
min_doppler_coverage: Minimum guaranteed one-sided Doppler reach (default:7000Hz). Both ends of the searched grid will be at least±min_doppler_coverage. Controlsnum_blocks— see Algorithm Constraints and Trade-offs for details.num_coherently_integrated_code_periods: Number of code periods per coherent integration block (default:1). Determines Doppler resolution:bin_spacing = 1 / (num_coherently_integrated_code_periods × T_code).bit_edge_search_steps: Number of bit-edge alignment candidates to search (default:1, disabled). Must dividebit_period_codes. Only valid whennum_coherently_integrated_code_periods > 1.num_noncoherent_accumulations: Number of successive incoherent integration steps (default:1). Signal passed toacquire!must contain at leastnum_noncoherent_accumulationsfull segments.use_secondary_code: enable the secondary-code rotation search (default:true). When the signal has a secondary code of lengthL > 1andnum_coherently_integrated_code_periods > 1, the search jointly estimates the unknown secondary-code start phase, recovering the full coherent gain.num_coherently_integrated_code_periodsmust then be a whole multiple ofL(e.g.10for GPS L5I's NH10). A partial secondary period introduces a ±Doppler sign ambiguity (a near-equal mirror peak at-f; issue #68) and is rejected with anArgumentError. Setfalseto opt out of the search (no constraint, coarser effective gain when a secondary code is present).max_secondary_code_rotations: cap on the secondary-code rotation-search size (default:32).plan_acquireerrors ifLexceeds this, before allocating buffers, so large-Lsignals (e.g. GPS L1C-P,L = 1800) fail fast.noise_estimator: howacquire!estimates noise power for the CFAR test statistic. Defaults toOppositeRowNoiseEstimator(averages one Doppler row, robust to Doppler-conditional banding from DC offset/IF spurs/narrowband interferers — recommended for real-world IF signals). PassGlobalMeanNoiseEstimatorfor synthetic-AWGN tests where the noise floor is independent and identically distributed across the grid and the lower variance of the global mean is preferred. See the Detecting Satellites section of the guide for the rationale.fft_flag: FFTW planning flag (default:FFTW.MEASURE).
See also
Acquisition.cfar_threshold — Function
cfar_threshold(pfa, num_cells; num_noncoherent_integrations=1)Compute the CFAR (Constant False Alarm Rate) detection threshold for a given probability of false alarm.
The threshold is computed so that the probability of any noise-only cell in the search grid exceeding the threshold equals pfa. This accounts for the multiple hypothesis testing across all num_cells search cells (code phases × Doppler bins) using a Bonferroni-like correction.
The test statistic is peak_power / noise_power (the peak_to_noise_ratio field of AcquisitionResults).
Arguments
pfa: Target probability of false alarm (e.g.,0.01). Must be in(0, 1).num_cells: Total number of search cells (typicallynum_code_phases * num_doppler_bins).
Keyword Arguments
num_noncoherent_integrations: Number of non-coherent integration dwells (default:1). More dwells increase the degrees of freedom of the chi-squared distribution.
Returns
Threshold value to compare against result.peak_to_noise_ratio.
Example
using Acquisition, GNSSSignals
system = GPSL1CA()
(; signal, sampling_freq, interm_freq) = generate_test_signal(system, 1)
results = acquire(system, signal, sampling_freq, 1:32; interm_freq)
detected = filter(is_detected, results)Mathematical Background
Under the null hypothesis (noise only), each power bin |I|² + |Q|² is χ²(2)-distributed with mean 2 (Springer Handbook of GNSS, Eq. 14.26). After M non-coherent integrations the raw power S_nc ~ χ²(2M) (Eq. 14.28). The test statistic peak_to_noise_ratio = S_nc / noise_power where noise_power ≈ 2M, so peak_to_noise_ratio ~ Gamma(M, 1/M) with mean 1 under H0.
The per-cell false alarm probability is adjusted for multiple testing: pfa_per_cell = 1 - (1 - pfa)^(1/num_cells).
The threshold is then the inverse CDF (quantile) of the chi-squared distribution at 1 - pfa_per_cell.
References
- Springer Handbook of GNSS, Section 14.3.1 "Test Statistics", Eq. 14.28–14.30
- GNSS-SDR PCPS Acquisition:
pcps_acquisition::calculate_threshold() - Kay Borre et al., "A Software-Defined GPS and Galileo Receiver", Birkhäuser, 2007
See also
Acquisition.is_detected — Function
is_detected(result::AcquisitionResults; pfa=0.01) -> BoolReturn true if the satellite signal is detected at the given probability of false alarm. Compares result.peak_to_noise_ratio against cfar_threshold, using the number of search cells and non-coherent integrations stored in the result.
Acquisition.get_num_cells — Function
get_num_cells(result::AcquisitionResults) -> IntReturn the number of search cells in the acquisition result. This is the num_cells argument expected by cfar_threshold.
The base grid is Doppler bins × code phases (num_blocks * block_size == samples_per_code). On the secondary-code rotation-search path the peak is taken over an expanded surface that carries num_secondary_rotations independent code-phase hypotheses per cell, so the count is multiplied by num_secondary_rotations (1 on every non-rotation path). Omitting this factor understates the cell count num_secondary_rotations-fold, which sets the CFAR threshold too low and inflates the realised false-alarm rate (issue #70).
Acquisition.generate_test_signal — Function
generate_test_signal(system, prn; kwargs...) -> NamedTupleGenerate a synthetic noisy GNSS signal. Useful for testing acquisition and demonstrating the API.
Returns a NamedTuple with fields: signal, code, carrier, num_samples, doppler, code_phase, sampling_freq, interm_freq, CN0, prn.
Keywords
seed=2345: Random seed for reproducibilitynum_samples=60000: Number of signal samplesdoppler=1234Hz: Carrier Doppler frequencycode_phase=110.613261: True code phase in chipssampling_freq=15e6Hz - 1Hz: Sampling frequencyinterm_freq=243.0Hz: Intermediate frequencyCN0=45: Carrier-to-noise density ratio (dB-Hz)phase_offset=π/8: Carrier phase offsetunit_noise_power=false: When true, scale so noise power ≈ 1; when false, scale so signal power corresponds directly to CN0
Example
using Acquisition, GNSSSignals
(; signal, prn, sampling_freq, interm_freq) = generate_test_signal(GPSL1CA(), 1)
result = acquire(GPSL1CA(), signal, sampling_freq, prn; interm_freq)
is_detected(result) # trueAcquisition.recommend_sampling_freqs — Function
recommend_sampling_freqs(code_length, code_freq;
fs_min = code_freq,
fs_max = 1.5 * fs_min,
min_doppler_coverage = 7000Hz,
num_coherently_integrated_code_periods = 1,
num_alternatives = 5,
max_prime = 7,
sort_by = :cost,
fs_step = 1000Hz,
sdr_clock_plan = nothing,
) -> Vector{SamplingFreqRecommendation}Search the range [fs_min, fs_max] for sampling frequencies with smooth-prime factorizations and return the num_alternatives best candidates.
The smallest valid num_blocks (the one plan_acquire would pick) is computed for every candidate, and candidates are ranked by either FFT cost or factorization smoothness. Candidates with the same samples_per_code are deduplicated — the first matching sampling_freq is reported.
Arguments
code_length: Number of chips per PRN code periodcode_freq: Code chipping rate (a frequency, e.g.1.023e6Hz)
Keyword Arguments
fs_min,fs_max: Sweep bounds for the sampling frequencymin_doppler_coverage: Minimum guaranteed one-sided Doppler reach. The recommender mirrorsplan_acquire— both ends of the searched grid will be at least±min_doppler_coverage.num_coherently_integrated_code_periods: Number of code periods per coherent integration block. Affectsnum_doppler_binsand therefore the column-FFT cost.num_alternatives: Maximum number of candidates to return (default5)max_prime: Reject candidates whosesamples_per_code,inner_fft_size, ornum_doppler_binscontain a prime factor larger than this value (default7— FFTW's "fast" regime)sort_by::cost(default) ranks by estimated FFT FLOPs;:smoothnessranks by largest prime factor ofinner_fft_size(ties broken by cost)fs_step: Sampling-frequency sweep step (default1000Hz)sdr_clock_plan: OptionalAbstractSDRClockPlandescribing hardware sample-rate constraints. When provided, the sweep range is intersected withsample_rate_range(plan)and every candidate must satisfyis_valid_sample_rate(plan, fs). Defaultnothing— no hardware filtering.
Cost Model
Both FFT stages of FM-DBZP are accounted for:
inner_fft_cost = 2 × num_blocks × inner_fft_size × log₂(inner_fft_size)
column_fft_cost = samples_per_code × num_doppler_bins × log₂(num_doppler_bins)
cost = inner_fft_cost + column_fft_costThis mirrors the per-PRN per coherent-step work, ignoring constant factors.
See also
recommend_sampling_freqs(::AbstractGNSSSignal), plan_acquire
recommend_sampling_freqs(system::AbstractGNSSSignal; kwargs...) -> Vector{SamplingFreqRecommendation}Convenience method that derives code_length and code_freq from system via get_code_length(system) and get_code_frequency(system).
All keyword arguments are forwarded to recommend_sampling_freqs(::Integer, ::Any).
Acquisition.is_valid_sample_rate — Function
is_valid_sample_rate(plan::AbstractSDRClockPlan, fs_hz::Real) -> BoolReturn true if fs_hz is reachable by the SDR described by plan.
Acquisition.sample_rate_range — Function
sample_rate_range(plan::AbstractSDRClockPlan) -> Tuple{Float64,Float64}Return (fs_min_hz, fs_max_hz), the inclusive sample-rate envelope of the SDR. Used by recommend_sampling_freqs to clamp the sweep range. The default fallback returns (0.0, Inf) so concrete plans only need to override it when there's a meaningful envelope.
Types
Acquisition.AcquisitionResults — Type
AcquisitionResults{S,T}Results from GNSS signal acquisition for a single PRN.
Fields
system::S: GNSS system used for acquisitionprn::Int: PRN number of the satellitesampling_frequency: Sampling frequency of the signalcarrier_doppler: Estimated carrier Doppler frequencycode_phase::Float64: Estimated code phase in chipssecondary_code_phase::Union{Int,Nothing}: Secondary-code phase index — the secondary-code chip the start of the coherent window aligned to, in0:get_secondary_code_length(system)-1. Estimated exactly by despreading the per-period prompt correlations at the recovered(carrier_doppler, code_phase).nothingwhen no rotation search ran (no secondary code,use_secondary_code = false, ornum_coherently_integrated_code_periods == 1) or when the peak does not clear the CFAR detection threshold (a non-detected peak has no meaningful secondary phase). Useful for seeding a tracking loop's secondary-code alignment.CN0::Float64: Carrier-to-noise density ratio in dB-Hznoise_power::T: Estimated noise powerpeak_to_noise_ratio::T: Ratio of peak correlation power to estimated noise power (peak_power / noise_power). Compare againstcfar_thresholdto decide if a satellite is present.num_noncoherent_integrations::Int: Number of non-coherent integrations performedpower_bins::Union{Matrix{T}, Nothing}: Correlation power over Doppler × code phase (for plotting). Only populated whenacquire!was called withstore_power_bins = true;nothingotherwise — by default the pipeline streams the reduction and never materialises this surface.dopplers: Doppler frequencies searchednum_blocks::Int: FM-DBZP number of blocks per code periodblock_size::Int: FM-DBZP samples per blocknum_secondary_rotations::Int: Secondary-code rotation hypotheses per cell (1off the rotation-search path). Part of the CFAR cell count — seeget_num_cells.
Plotting
AcquisitionResults can be plotted directly using Plots.jl:
using Plots
plot(result) # heatmap of correlation power (chip-axis sorted)
plot(result, true) # Use log scale (dB)See also
Acquisition.AcquisitionPlan — Type
AcquisitionPlanPre-computed acquisition plan for FM-DBZP (Heckler & Garrison 2009).
See plan_acquire and acquire.
Acquisition.SamplingFreqRecommendation — Type
SamplingFreqRecommendationA single sampling-frequency candidate from recommend_sampling_freqs.
Fields
sampling_freq::typeof(1.0Hz): Sampling frequencysamples_per_code::Int: Samples per PRN code period at thissampling_freqnum_blocks::Int: Smallest validnum_blocksthat meetsmin_doppler_coverage(= the valueplan_acquirewould pick)block_size::Int:samples_per_code ÷ num_blocksinner_fft_size::Int: Inner double-block FFT length (2 × block_size)num_doppler_bins::Int: Column-FFT length (num_coherently_integrated_code_periods × num_blocks)doppler_coverage::typeof(1.0Hz): Total Doppler coverage at this configurationinner_max_prime::Int: Largest prime factor ofinner_fft_size— smaller is better for FFTWnum_doppler_bins_max_prime::Int: Largest prime factor ofnum_doppler_binscost::Float64: Estimated combined FFT cost (inner_fft_cost + column_fft_cost, seerecommend_sampling_freqs)
Acquisition.AbstractSDRClockPlan — Type
AbstractSDRClockPlanHardware constraints that decide which sampling frequencies an SDR can produce.
Concrete subtypes implement is_valid_sample_rate. They may also override sample_rate_range to narrow the sweep range.
See AD9361ClockPlan for an example implementation.
Acquisition.AD9361ClockPlan — Type
AD9361ClockPlan(; ref_clk = 38.4e6, adc_clk_min = 25e6, adc_clk_max = 640e6,
fs_min = 0.55e6, fs_max = 61.44e6,
bbpll_min = 715e6, bbpll_max = 1430e6,
bbpll_div_min = 2, bbpll_div_max = 64,
dividers = [12, 8, 6, 4, 3, 2, 1])Sample-rate validation for the Analog Devices AD9361 RFIC, as used by the LiteX-M2SDR and many other SDR boards.
A sample rate fs is reachable when some divider d ∈ dividers makes adc_clk = fs × d land in [adc_clk_min, adc_clk_max], and that adc_clk can be reached from the BBPLL: adc_clk × bbpll_div ∈ [bbpll_min, bbpll_max] for some power-of-two bbpll_div ∈ [bbpll_div_min, bbpll_div_max].
The defaults reproduce the limits in the AD9361 driver shipped with litex_m2sdr (see software/user/ad9361/ad9361.h).
Noise Estimators
Acquisition.AbstractNoiseEstimator — Type
AbstractNoiseEstimatorSingleton type marking which noise-power estimator is used by acquire! when computing the CFAR test statistic. Stored in AcquisitionPlan so the choice is fixed at plan-construction time and flows through every subsequent acquire! call.
Pass to plan_acquire via the noise_estimator keyword:
plan = plan_acquire(system, sampling_freq, prns;
noise_estimator = OppositeRowNoiseEstimator()) # default
plan = plan_acquire(system, sampling_freq, prns;
noise_estimator = GlobalMeanNoiseEstimator()) # alternativeAcquisition.OppositeRowNoiseEstimator — Type
OppositeRowNoiseEstimator()Default noise estimator. Averages the cells of one Doppler row across all code-phase columns — specifically the row exactly half the search grid away from the peak. That single row is then taken as the noise floor estimate.
Why this is the default
The CFAR test statistic is peak_power / noise_power, so a biased noise estimate translates directly into FP/FN errors. On real-world IF signals the search-grid noise floor is not independent and identically distributed across Doppler: DC offset, IF spurs and narrowband interferers inflate specific Doppler rows ("Doppler-conditional banding"). Averaging over the whole grid then mixes hot rows into the noise estimate, the threshold rises with the hot rows, and any peak whose Doppler happens to land in a quiet row passes the inflated threshold — hence false alarms cluster at specific Doppler offsets.
Estimating the noise from a single Doppler row removes that bias: the estimate is conditioned on Doppler, so the threshold tracks the local noise floor. The opposite row (peak Doppler bin offset by num_doppler_bins ÷ 2) is the natural choice — it is statistically far from any signal mainlobe and cannot be contaminated by a real signal's sidelobes either. This mirrors GNSS-SDR's pcps_acquisition::max_to_input_power_statistic.
The downside is a slightly larger variance on the noise estimate than the global mean (one row of samples_per_code cells, vs. the full grid), but the bias removal dominates: in practice the false-alarm rate on Doppler-banded signals drops by an order of magnitude or more.
When to use GlobalMeanNoiseEstimator instead
Synthetic AWGN test signals where the noise floor is genuinely flat across Doppler. The global mean uses ~num_doppler_bins× more samples and so has a tighter (lower-variance) estimate when the noise really is independent and identically distributed across the grid.
Acquisition.GlobalMeanNoiseEstimator — Type
GlobalMeanNoiseEstimator()Alternative noise estimator. Averages every cell of the search grid except a small ±samples_per_chip exclusion zone around the peak column.
This matches the original CFAR formula derivation: when the per-cell noise power is independent and identically distributed, all num_cells are exchangeable and the unbiased noise-power estimate is the sample mean over the cells. With a large grid the variance of this estimate is essentially zero, which makes it the lowest-variance estimator when that assumption holds.
When to use this
Synthetic AWGN test signals with no Doppler-conditional structure (generated by generate_test_signal with white noise, for example).
When NOT to use this
Real-world IF recordings. Hardware-induced effects make the noise floor Doppler-dependent — DC offset, IF spurs, narrowband interferers, ADC nonlinearity all inflate specific rows of the grid. The global mean folds those hot rows into the noise estimate, the threshold rises with them, and peaks landing in quiet rows pass it — producing clusters of false alarms at predictable Doppler offsets. Prefer OppositeRowNoiseEstimator in that case (it is the default).
Index
Acquisition.AD9361ClockPlanAcquisition.AbstractNoiseEstimatorAcquisition.AbstractSDRClockPlanAcquisition.AcquisitionPlanAcquisition.AcquisitionResultsAcquisition.GlobalMeanNoiseEstimatorAcquisition.OppositeRowNoiseEstimatorAcquisition.SamplingFreqRecommendationAcquisition.acquireAcquisition.acquire!Acquisition.cfar_thresholdAcquisition.generate_test_signalAcquisition.get_num_cellsAcquisition.is_detectedAcquisition.is_valid_sample_rateAcquisition.plan_acquireAcquisition.recommend_sampling_freqsAcquisition.sample_rate_range