API Reference

Acquisition Functions

Acquisition.acquireFunction
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 samples
  • sampling_freq: Sampling frequency
  • prns: 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). Requires num_coherently_integrated_code_periods to be a whole multiple of the secondary-code length L; a partial period is rejected to avoid the ±Doppler sign ambiguity (issue #68). See plan_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

plan_acquire, acquire!

source
acquire(system, signal, sampling_freq, prn::Integer; kwargs...) -> AcquisitionResults

Single-PRN convenience method. Returns a single AcquisitionResults.

source
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-computed AcquisitionPlan (from plan_acquire)
  • signal: Complex baseband signal samples
  • prns: PRN numbers to search (must be a subset of plan.avail_prns)

Keyword Arguments

  • interm_freq: Intermediate frequency (default: 0.0Hz)
  • subsample_interpolation: When true, apply parabolic interpolation to refine the code phase and Doppler estimates below the grid resolution (default: false)
  • store_power_bins: When true, copy the full Doppler × code-phase correlation power matrix into each result's power_bins field (required for plotting). When false, power_bins is nothing and no extra copy is made (default: false)

Returns

Vector{AcquisitionResults}, one entry per PRN in prns.

See also

acquire, plan_acquire

source
acquire!(plan::AcquisitionPlan, signal, prn::Integer; kwargs...) -> AcquisitionResults

Single-PRN convenience method. Calls the multi-PRN acquire! and returns the single result.

source
Acquisition.plan_acquireFunction
plan_acquire(system, sampling_freq, prns; kwargs...) -> AcquisitionPlan

Pre-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 frequency
  • prns: 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. Controls num_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 divide bit_period_codes. Only valid when num_coherently_integrated_code_periods > 1.
  • num_noncoherent_accumulations: Number of successive incoherent integration steps (default: 1). Signal passed to acquire! must contain at least num_noncoherent_accumulations full segments.
  • use_secondary_code: enable the secondary-code rotation search (default: true). When the signal has a secondary code of length L > 1 and num_coherently_integrated_code_periods > 1, the search jointly estimates the unknown secondary-code start phase, recovering the full coherent gain. num_coherently_integrated_code_periods must then be a whole multiple of L (e.g. 10 for 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 an ArgumentError. Set false to 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_acquire errors if L exceeds this, before allocating buffers, so large-L signals (e.g. GPS L1C-P, L = 1800) fail fast.
  • noise_estimator: how acquire! estimates noise power for the CFAR test statistic. Defaults to OppositeRowNoiseEstimator (averages one Doppler row, robust to Doppler-conditional banding from DC offset/IF spurs/narrowband interferers — recommended for real-world IF signals). Pass GlobalMeanNoiseEstimator for 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

acquire!, acquire

source
Acquisition.cfar_thresholdFunction
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 (typically num_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

AcquisitionResults, acquire

source
Acquisition.is_detectedFunction
is_detected(result::AcquisitionResults; pfa=0.01) -> Bool

Return 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.

source
Acquisition.get_num_cellsFunction
get_num_cells(result::AcquisitionResults) -> Int

Return 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).

source
Acquisition.generate_test_signalFunction
generate_test_signal(system, prn; kwargs...) -> NamedTuple

Generate 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 reproducibility
  • num_samples=60000: Number of signal samples
  • doppler=1234Hz: Carrier Doppler frequency
  • code_phase=110.613261: True code phase in chips
  • sampling_freq=15e6Hz - 1Hz: Sampling frequency
  • interm_freq=243.0Hz: Intermediate frequency
  • CN0=45: Carrier-to-noise density ratio (dB-Hz)
  • phase_offset=π/8: Carrier phase offset
  • unit_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)  # true
source
Acquisition.recommend_sampling_freqsFunction
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 period
  • code_freq: Code chipping rate (a frequency, e.g. 1.023e6Hz)

Keyword Arguments

  • fs_min, fs_max: Sweep bounds for the sampling frequency
  • min_doppler_coverage: Minimum guaranteed one-sided Doppler reach. The recommender mirrors plan_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. Affects num_doppler_bins and therefore the column-FFT cost.
  • num_alternatives: Maximum number of candidates to return (default 5)
  • max_prime: Reject candidates whose samples_per_code, inner_fft_size, or num_doppler_bins contain a prime factor larger than this value (default 7 — FFTW's "fast" regime)
  • sort_by: :cost (default) ranks by estimated FFT FLOPs; :smoothness ranks by largest prime factor of inner_fft_size (ties broken by cost)
  • fs_step: Sampling-frequency sweep step (default 1000Hz)
  • sdr_clock_plan: Optional AbstractSDRClockPlan describing hardware sample-rate constraints. When provided, the sweep range is intersected with sample_rate_range(plan) and every candidate must satisfy is_valid_sample_rate(plan, fs). Default nothing — 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_cost

This mirrors the per-PRN per coherent-step work, ignoring constant factors.

See also

recommend_sampling_freqs(::AbstractGNSSSignal), plan_acquire

source
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).

source
Acquisition.sample_rate_rangeFunction
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.

source

Types

Acquisition.AcquisitionResultsType
AcquisitionResults{S,T}

Results from GNSS signal acquisition for a single PRN.

Fields

  • system::S: GNSS system used for acquisition
  • prn::Int: PRN number of the satellite
  • sampling_frequency: Sampling frequency of the signal
  • carrier_doppler: Estimated carrier Doppler frequency
  • code_phase::Float64: Estimated code phase in chips
  • secondary_code_phase::Union{Int,Nothing}: Secondary-code phase index — the secondary-code chip the start of the coherent window aligned to, in 0:get_secondary_code_length(system)-1. Estimated exactly by despreading the per-period prompt correlations at the recovered (carrier_doppler, code_phase). nothing when no rotation search ran (no secondary code, use_secondary_code = false, or num_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-Hz
  • noise_power::T: Estimated noise power
  • peak_to_noise_ratio::T: Ratio of peak correlation power to estimated noise power (peak_power / noise_power). Compare against cfar_threshold to decide if a satellite is present.
  • num_noncoherent_integrations::Int: Number of non-coherent integrations performed
  • power_bins::Union{Matrix{T}, Nothing}: Correlation power over Doppler × code phase (for plotting). Only populated when acquire! was called with store_power_bins = true; nothing otherwise — by default the pipeline streams the reduction and never materialises this surface.
  • dopplers: Doppler frequencies searched
  • num_blocks::Int: FM-DBZP number of blocks per code period
  • block_size::Int: FM-DBZP samples per block
  • num_secondary_rotations::Int: Secondary-code rotation hypotheses per cell (1 off the rotation-search path). Part of the CFAR cell count — see get_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

acquire, plan_acquire

source
Acquisition.SamplingFreqRecommendationType
SamplingFreqRecommendation

A single sampling-frequency candidate from recommend_sampling_freqs.

Fields

  • sampling_freq::typeof(1.0Hz): Sampling frequency
  • samples_per_code::Int: Samples per PRN code period at this sampling_freq
  • num_blocks::Int: Smallest valid num_blocks that meets min_doppler_coverage (= the value plan_acquire would pick)
  • block_size::Int: samples_per_code ÷ num_blocks
  • inner_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 configuration
  • inner_max_prime::Int: Largest prime factor of inner_fft_size — smaller is better for FFTW
  • num_doppler_bins_max_prime::Int: Largest prime factor of num_doppler_bins
  • cost::Float64: Estimated combined FFT cost (inner_fft_cost + column_fft_cost, see recommend_sampling_freqs)
source
Acquisition.AD9361ClockPlanType
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).

source

Noise Estimators

Acquisition.AbstractNoiseEstimatorType
AbstractNoiseEstimator

Singleton 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())   # alternative

See OppositeRowNoiseEstimator and GlobalMeanNoiseEstimator.

source
Acquisition.OppositeRowNoiseEstimatorType
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.

source
Acquisition.GlobalMeanNoiseEstimatorType
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).

source

Index