Skip to content

File acq_core.h

FileList > acq > acq_core.h

Go to the source code of this file

Streaming DSSS acquisition engine — burst and continuous front doors over one shared engine. More...

  • #include "doppler/clib_common.h"
  • #include "doppler/corr2d/corr2d_core.h"
  • #include "doppler/detection/detection_core.h"
  • #include "doppler/dp_state.h"
  • #include "doppler/f32_buffer/f32_buffer_core.h"
  • #include "doppler/fft/fft_core.h"
  • #include "doppler/jm_perf.h"
  • #include "doppler/detector2d/detector2d_core.h"
  • #include "doppler/fft2d/fft2d_core.h"
  • #include "doppler/dp_parallel.h"
  • #include "doppler/dp_tlm/dp_tlm_core.h"
  • #include "doppler/cvt/cvt_core.h"

Classes

Type Name
struct acq_extra_t
Per-object extra header for an engine's cross-call state.
struct acq_handoff_t
Wire-ready hand-off record built from one acq_result_t hit.
struct acq_part_t
One tile's share of a decided surface (design §2.3): the surface is cut into window_bins chunks of whole rows, and the per-cell passes after the fan the magnitude, the CFAR reference, the mask copy, each scan of the peak list run per chunk into one of these, merged serially in tile order. The merge is bit-identical at any thread count because the chunks never move.
struct acq_result_t
One acquisition detection event.
struct acq_shape_t
What the engine knows about the SHAPE of the repeated preamble, beyond its samples (doppler#1470).
struct acq_tlm_t
Telemetry attachment: a borrowed context + this engine's probe ids (design §2.4). NULL ctx (the default) means detached — the one probe site is then a single predicted-not-taken branch per decided dwell. Never in a state blob; preserved across dp_acq_set_state() like the borrowed code.
struct dp_acq_state_t
Streaming acquisition-engine state.

Public Types

Type Name
typedef void(* acq_surface_sink_fn
A surface sink: called once per decided dwell (every decim -th) with the dwell's surface in test-statistic units, row-majorrows xcols (Doppler rows by code-phase columns), and the dwell'ssamples_consumed . The pointer is the engine's and is valid only for the call. Seedp_acq_set_surface_sink() .

Public Functions

Type Name
size_t dp_acq_block_prompt (dp_acq_state_t * state, size_t tile, size_t col, float _Complex * out, size_t n_out)
One cell's column of the last whole block: the per-epoch complex correlations at a code phase, the despread stream at epoch rate.
size_t dp_acq_block_raw (dp_acq_state_t * state, float _Complex * out, size_t n_out)
The last whole block's raw samples, as pushed.
void dp_acq_build_handoff (const dp_acq_state_t * state, const acq_result_t * hit, size_t code_len, size_t spc, acq_handoff_t * out)
Convert one dp_acq_push() hit into a wire-ready hand-off record.
int dp_acq_carrier_freq_ok (double carrier_freq_hz, double sample_rate_hz)
Whether carrier_freq_hz is a carrier the code can be coupled to atsample_rate_hz: 0 (uncoupled), or finite and above half the sample rate.
double _Complex dp_acq_cell_corr (const dp_acq_state_t * state, const float _Complex * x, size_t col, double f_hz, double t0)
One epoch's correlation against the replica at ONE code phase and ONE frequency: the engine's surface, evaluated at a single cell, on raw samples.
void dp_acq_cell_corr_grid (const dp_acq_state_t * state, const float _Complex * x, size_t n_epochs, size_t col, const double * f_hz, size_t n_f, double t0, double _Complex * out)
dp_acq_cell_corr() at many frequencies over consecutive epochs, in one pass: the cells a caller scores once the code phase is settled and only the Doppler is left to search.
int dp_acq_configure_search_raw (dp_acq_state_t * state, size_t doppler_bins, size_t n_noncoh)
Pin the search grid directly, bypassing both auto-sizing searches — the advanced escape hatch (mirrors Dll's/Costas's configure_lock_raw()).
size_t dp_acq_consumed (const dp_acq_state_t * state)
Input samples the last dp_acq_push() took.
dp_acq_state_t * dp_acq_create_burst (const float _Complex * tmpl, size_t n, size_t reps, double fs, double cn0_dbhz, double doppler_uncertainty, double pfa, double pd, int noise_mode, double doppler_rate, size_t rate_hypotheses)
Create a burst-mode acquisition engine for a repeated preamble: coherent multi-repetition combining, up to reps deep.
dp_acq_state_t * dp_acq_create_continuous (const uint8_t * code, size_t code_len, size_t spc, double chip_rate, double symbol_rate, double cn0_dbhz, double doppler_uncertainty, double pfa, double pd, int noise_mode, size_t code_only_epochs, double doppler_rate)
Create a continuous-mode acquisition engine: always wideband window-tiling, allowing a block-coherent depth inside the tiles to accommodate waveforms with code-only windows.
void dp_acq_destroy (dp_acq_state_t * state)
Destroy and free an engine.
double dp_acq_dsl_db (dp_acq_state_t * state)
The search's Doppler-offset sidelobe level, dB: 20*log10 of the largest lag of the code's periodic ambiguity outside the mainlobe, at any Doppler offset the grid's tiles can put between an emitter and a tile, relative to the emitter's own peak ( acq_shape_t::dsl ).
double dp_acq_gate_stat (const dp_acq_state_t * state)
The gate a dwell's statistic must clear to be a hit, in the units of acq_result_t::test_stat :threshold on the coherent path (one look),eta_nc on the non-coherent one. A dwell hits when its strongest cell's statistic exceeds this, and every listed peak's statistic does too. Both statistics are linear in the cell's amplitude, so a list consumer can compare a peak's excess over another's against it in the same units. Configuration: fixed by the thresholds the grid was sized with.
void dp_acq_get_state (const dp_acq_state_t * state, void * blob)
Serialize state's cross-call state intoblob (caller-owned,dp_acq_state_bytes() long). Call between pushes (no partial dump pending).
uint64_t dp_acq_position (const dp_acq_state_t * state)
The engine's stream position: every sample its pushes have taken, framed ones and the carry, since create, reset, a regrid or set_state's restore.
double dp_acq_psl_db (const dp_acq_state_t * state)
The preamble's peak sidelobe level, dB: 20*log10 of the largest periodic-autocorrelation lag outside the mainlobe, relative to the peak ( acq_shape_t::psl ).
size_t dp_acq_push (dp_acq_state_t * state, const float _Complex * x, size_t n_in, acq_result_t * result, size_t max_results)
Stream raw samples; emit one event per CFAR dump above threshold.
size_t dp_acq_rate_hypotheses_for (double f_epoch, double span, size_t depth)
The fewest rate hypotheses over [-span, +span] that leave the residualspan/(n - 1) within what a block ofdepth epochs tolerates,f_epoch^2/(2*depth^2) __ the drift rule behind the depth cap, inverted. At least 2 for a positive span; 1 for none; at mostACQ_RATE_HYPS_MAX .
double dp_acq_rate_hypothesis (double span, size_t n, size_t h)
Rate hypothesis h ofn over[-span, +span] , Hz/s:-span + 2*span*h/(n - 1) ; 0 forn <= 1 .
double _Complex dp_acq_rate_phasor (double rate, double t)
The de-chirp phasor exp(-j*pi*rate*t^2) for a rate in Hz/s at timet seconds from the block's middle.
void dp_acq_reset (dp_acq_state_t * state)
Drain the input ring and reset the coherent accumulator.
size_t dp_acq_run (dp_acq_state_t * state, const void * state_in, void * state_out, const float _Complex * in, size_t n_in, acq_result_t * result, size_t max_results)
Pure run: inject state_in , streamin , emit hits, exportstate_out —(state_in, input) -> (state_out, output) over an engine treated as immutable config + scratch.state_in /state_out may alias. Either may be NULL (NULL in = fresh; NULL out = discard).
int dp_acq_set_carrier_freq_hz (dp_acq_state_t * state, double carrier_freq_hz)
Couple the code clock to the carrier: the chip rate dilates by doppler_hz / carrier_freq_hz , and the engine accounts for it.
int dp_acq_set_max_peaks (dp_acq_state_t * state, size_t n)
How many peaks a dwell may report: the peak list's capacity.
int dp_acq_set_state (dp_acq_state_t * state, const void * blob)
Restore cross-call state from blob intostate (replacing it).
void dp_acq_set_surface_sink (dp_acq_state_t * state, acq_surface_sink_fn fn, void * ctx, uint32_t decim)
Attach (or detach) a C surface sink: every decim-th decided dwell's surface, in test-statistic units, handed tofn on the pushing thread (design §2.4).
int dp_acq_set_telemetry (dp_acq_state_t * state, dp_tlm_t * tlm, const char * prefix, uint32_t decim)
Attach (or detach) a telemetry context and register the engine's probes on it (design §2.4).
int dp_acq_set_threads (dp_acq_state_t * state, int n)
Set how many threads the searcher fans its tiles across (design §2.3: a roll per thread on persistent workers).
size_t dp_acq_state_bytes (const dp_acq_state_t * state)
Byte size of state's blob: the envelope and the engine's position, the framer's snapshot of the carry, the non-coherent sum (n_noncoh > 1), the last dwell's twins, and a tiled block's epochs.
size_t dp_acq_surface (dp_acq_state_t * state, float * out, size_t n_out)
The last decided dwell's surface, in the gate's own units.
size_t dp_acq_surface_chip_phase (dp_acq_state_t * state, double * out, size_t n_out)
The surface's code-phase axis: the chip phase of each column.
size_t dp_acq_surface_complex (dp_acq_state_t * state, float _Complex * out, size_t n_out)
The last decided dwell's surface, complex: amplitude and carrier phase per cell, before the magnitude the gate reads.
size_t dp_acq_surface_doppler_hz (dp_acq_state_t * state, double * out, size_t n_out)
The surface's Doppler axis: the frequency of each row, in Hz.

Public Static Functions

Type Name
double acq_bin_doppler_hz (const dp_acq_state_t * state, size_t doppler_bin)
A hit's signed Doppler, Hz: its doppler_bin folded over acq_grid_bins() by numpy's fftfreq convention (dp_fftfreq_index) and scaled by the grid's resolution.
size_t acq_grid_bins (const dp_acq_state_t * state)
The Doppler grid a hit's doppler_bin indexes:window_bins * coherent_bins bins.

Macros

Type Name
define ACQ_CN0_NONE NAN
The design C/N0 that means "none given" (doppler#1484): a burst engine then sizes for the whole preamble, its pd_predicted is NaN and it is neverunderpowered .
define ACQ_COL_CHUNK 32u
define ACQ_DELAY_LOSS_NODES 4
define ACQ_MAX_PEAKS 64u
define ACQ_N_NONCOH_SAFETY_CEILING 256u
Internal safety-valve ceiling on auto-selected non-coherent looks not a public knob (no caller-facing equivalent of the retiredmax_noncoh parameter).
define ACQ_RATE_HYPS_MAX 64u
define ACQ_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('A', 'C', 'Q', 'R')
define ACQ_STATE_VERSION 6u

Detailed Description

Acquires a signal built from a repeated sequence a spread-spectrum code (the continuous door) or any repeated complex preamble (the burst door) arriving with an unknown integer code phase and an unknown carrier-frequency (Doppler) offset, buried in AWGN. It jointly estimates the (Doppler bin, code phase) and declares a detection whenever the CFAR test statistic crosses an automatically configured threshold.

Pipeline (owned end to end, one object): push(raw cf32) -> ring buffer -> reframe to (coherent_bins, code_bins) -> slow-time Doppler FFT (FFT along the segment axis) -> 2-D code correlation against a single-row reference (corr2d) -> argmax + CFAR noise estimate -> threshold gate -> acq_result_t.

The fast-time axis (code_bins = sf*spc columns) is the circular code matched filter; the slow-time axis (coherent_bins rows, one row per code repetition) is the coherent Doppler search. A carrier offset f (cycles/sample) lands the peak at row = round(f*code_bins*coherent_bins) mod coherent_bins, column = code phase.

Two mode-fixed public constructors, one shared engine. A coherent slow-time Doppler FFT can only ever resolve frequency within one native span chip_rate/(2*sf) — more coherent depth subdivides that SAME fixed range more finely, it never widens it — and for a continuous (async, data-modulated) signal, a multi-epoch coherent axis wide enough to matter aliases the data's own bit transitions across the whole Doppler-bin axis (a structural mislock, not a graceful SNR loss — see docs/design/dsss-acquisition.md). So the two constructors fix a mode each, never a per-call knob:

  • dp_acq_create_burst() — the smallest coherent depth coherent_bins in [1, reps] whose BURST Pd meets pd: the preamble lands at any offset against stream-aligned dwells and is detected when any one of them is (see dp_acq_create_burst()); never a non-coherent look. Minimum latency for a strong signal, unmodulated bursts/preambles only. A tighter doppler_uncertainty shrinks the searched cell count, lowering the Sidak threshold (more sensitive). When doppler_uncertainty exceeds the native span, the wideband window-tiling mechanism below tiles it, and the coherent depth is chosen INSIDE every tile the smallest that meets pd at a stated design C/N0, and 1 with none (a default-built tiled search has never carried a block).
  • dp_acq_create_continuous() — for a continuous, data-modulated signal: ALWAYS uses the wideband window-tiling mechanism below, unconditionally (never attempts coherent multi-epoch combining, even when doppler_uncertainty is narrower than one native span) — closes the aliasing footgun structurally rather than pricing it as a tunable loss. Sensitivity margin comes entirely from auto-selected non-coherent looks.

Both convert C/N0 to a per-sample amplitude SNR (snr = sqrt(10^(cn0_dbhz/10) / (chip_rate*spc))). Every reported detection inverts this same relationship to report an estimated C/N0 (acq_result_t::cn0_dbhz_est) — a bandwidth/integration-time-independent figure of merit directly comparable to cn0_dbhz, unlike a raw per-sample or coherently-integrated ratio (both scale with spc/ reps and so aren't portable across configurations).

Wideband window-tiling mode: instead of coherent combining, tiles the requested uncertainty with window_bins = 2*ceil((du - span)/(2*span)) + 1 (du = doppler_uncertainty, span = chip_rate/(2*sf); always odd, so the coverage is symmetric) parallel frequency-window hypotheses, each one native span wide, searched every epoch from a SINGLE shared forward FFT of that epoch: hypothesis r's spectrum is the shared FFT circularly rolled by r bins (exact — the window spacing IS this code_bins-point FFT's own bin spacing) against one fixed precomputed replica spectrum, then inverse-FFT'd — window_bins inverse FFTs plus the one shared forward FFT per epoch, not window_bins independent down-conversions. Empirically the cheaper of the two realizations benchmarked for this (a frequency-bank benchmark): ~1.2x-1.55x faster than an equivalent tuned-mixer bank, measured with real doppler.spectral.FFT. SNR margin in this mode comes entirely from auto-selected non-coherent looks (magnitude-squared accumulation, immune to data-modulation sign flips) rather than coherent depth, sized against an internal safety-valve ceiling rather than a caller-supplied cap (the semi-analytical Pd model this engine sizes against grows unreliable past a few hundred looks — not a public knob to tune around that). doppler_bin in acq_result_t reports the frequency-window index (0 … window_bins-1, native FFT-bin ordering) instead of a slow-time-FFT row when this mode is active at coherent_bins=1, where doppler_res_hz is the per-window spacing (chip_rate/sf). A burst engine sized to a coherent depth D > 1 inside the tiles (a stated design C/N0) indexes the combined grid instead: window_bins*D bins of chip_rate/(sf*D), folded over acq_grid_bins().

Block-coherent depth inside the tiles (docs/design/async-dsss-receiver.md §2.3): the engine allows a coherent depth, to accommodate waveforms with code-only windows. Given code_only_epochs > 1 the whole code-only epochs such a window holds at any chip phase it runs a coherent depth D inside every tile: the per-tile epoch correlations are gathered for D epochs, then a zero-padded slow-time FFT per code-phase column turns each tile into D Doppler rows chip_rate/(sf*D) apart, detected per block. Blocks are non-overlapping and the engine does not know any emitter's window phase, so D is at most (code_only_epochs+1)/2 (a whole block always lands inside the window) and, when doppler_rate is given, at most f_epoch/sqrt(2*doppler_rate) (the drift over a block stays inside half a row), rounded down to a 7-smooth length (dp_fft_smooth_floor()): D is a slow-time FFT length, and a prime factor of 13 or more costs pocketfft its slow path (doppler#2457). The Doppler axis is then ONE uniform grid of window_bins*coherent_bins bins of doppler_res_hz = chip_rate/(sf*D) over the tiled span, in native FFT-bin order (0 = DC, ascending, then wrapping negative): doppler_bin indexes it, and dp_acq_build_handoff() folds it with dp_fftfreq_index() over that count. A block that straddles data spreads that emitter over its rows, 10*log10(D) below an aligned block, at its own code phase the conc probe (§2.4) reads it. code_only_epochs = 1 (the default) is D = 1 and the engine exactly as described above. A tile de-rotates by its own centre, so an emitter on the edge between two tiles reads the same in both (within 0.03 dB) and the slow-time transform folds it to the same row index of each: every listed peak is therefore asked at its ROW's frequency before it is reported the block's raw epochs correlated with the replica at the pick's code phase, mixed by the row's frequency and by that one span down and up, the winner reported (design §2.3, docs/design/async-dsss-receiver-measurements.md §12.18, doppler#1270).

A roll per thread (design §2.3): the tiles are independent after the one forward transform, so the per-epoch tile loop and, at D > 1, the block-end column loop run across a persistent pool of workers (dp_parallel.h's dp_pool_*), created once with the engine and parked between pushes. Each tile owns its inverse plan and scratch, so the result is bit-identical at any thread count. The per-cell passes that decide a surface the magnitude, the CFAR reference, the working mask and every scan of the peak list run per tile too, each into a slot of its own, and are merged serially in tile order (a mean of the tiles' means over equal cells, the first of the tiles' first maxima), so the serial remainder is per tile, not per cell. dp_acq_set_threads() sets the count.

// 31-chip PN, 4x oversample, up to 16 coherent reps; 1 MHz chips, 45 dB-Hz
uint8_t code[31] = { 0 };   // ... fill with PN chips (0/1) ...
float nrz[31];
bin_to_nrz(code, 31, nrz, 31);           // chip 0 -> +1, chip 1 -> -1
float _Complex pre[124];
for (size_t i = 0; i < 124; i++)
  pre[i] = nrz[i / 4];                   // each chip held 4 samples
dp_acq_state_t *a = dp_acq_create_burst(pre, 124, 16, 4.0e6, 45.0,
                                  0.0, 1e-3, 0.9, 0, 0.0, 1);
acq_result_t hits[64];
size_t nh = dp_acq_push(a, samples, n_samples, hits, 64);
for (size_t i = 0; i < nh; i++)
  printf("Doppler %zu, code phase %zu, C/N0 %.1f dB-Hz\n",
         hits[i].doppler_bin, hits[i].code_phase,
         hits[i].cn0_dbhz_est);
dp_acq_destroy(a);

Public Types Documentation

typedef acq_surface_sink_fn

A surface sink: called once per decided dwell (every decim -th) with the dwell's surface in test-statistic units, row-majorrows xcols (Doppler rows by code-phase columns), and the dwell'ssamples_consumed . The pointer is the engine's and is valid only for the call. Seedp_acq_set_surface_sink() .

typedef void(* acq_surface_sink_fn) (void *ctx, const float *surface, size_t rows, size_t cols, uint64_t samples_consumed);


Public Functions Documentation

function dp_acq_block_prompt

One cell's column of the last whole block: the per-epoch complex correlations at a code phase, the despread stream at epoch rate.

size_t dp_acq_block_prompt (
    dp_acq_state_t * state,
    size_t tile,
    size_t col,
    float _Complex * out,
    size_t n_out
) 

The block-coherent engine gathers every tile's correlation row for coherent_bins epochs before its slow-time transform (file doc, design §2.3). This copies the coherent_bins values at column col of tile tile into out, in epoch order: each epoch's complex prompt at that code phase, rolled to the tile's centre frequency and shifted to the block's middle by the tile's code-rate hypothesis, so the phase is continuous along the column for an emitter at the tile's centre and rotates at its offset from it. At an emitter's cell this is what a despreader produces, one value per epoch, phase included (docs/design/async-dsss-receiver-measurements.md §12.21). Valid once a block is whole, until the next epoch is pushed.

Parameters:

  • state Must be non-NULL.
  • tile Tile index, 0 … window_bins-1 (native FFT order, the order the surface's rows are cut in).
  • col Code-phase column, 0 … code_bins-1.
  • out At least coherent_bins complex floats.
  • n_out Capacity of out.

Returns:

Values written (coherent_bins), or 0 at coherent_bins == 1 (no block is gathered), while a block is partial, for an index out of range, or when out is too small.

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(PN(poly=mls_poly(5), seed=1,
...                      length=5).generate(31)).astype(np.uint8)
>>> s0 = np.repeat(np.where(code & 1, -1.0, 1.0), 4).astype(
...     np.complex64)
>>> b = Acquisition(code, spc=4, chip_rate=1e6, cn0_dbhz=60.0,
...                 doppler_uncertainty=40e3, code_only_epochs=7)
>>> b.coherent_bins                    # (7 + 1) // 2
4
>>> blk = np.tile(np.roll(s0, 17), b.coherent_bins).astype(np.complex64)
>>> _ = b.push(blk)
>>> p = np.empty(b.coherent_bins, dtype=np.complex64)
>>> b.block_prompt(0, 17, p) == b.coherent_bins
True
>>> bool(np.abs(p).min() > 0.99 * np.abs(p).max())  # every epoch's prompt
True
>>> b.block_prompt(0, 17, np.empty(1, dtype=np.complex64))  # too small
0


function dp_acq_block_raw

The last whole block's raw samples, as pushed.

size_t dp_acq_block_raw (
    dp_acq_state_t * state,
    float _Complex * out,
    size_t n_out
) 

Copies the coherent_bins * code_bins samples the block-coherent engine gathered for its last whole block into out, epoch by epoch in stream order — the samples dp_acq_push() consumed, untouched. Kept for the tile-edge re-ask (acq_resolve_tile_alias()); exposed so a tracker can re-correlate them at any code phase, rate or symbol boundary the engine's own grid does not have — a symbol-rate despreader at the tracked timing runs on exactly this (docs/design/async-dsss-receiver-measurements.md §12.21). Valid once a block is whole, until the next epoch is pushed.

Parameters:

  • state Must be non-NULL.
  • out At least coherent_bins * code_bins complex floats.
  • n_out Capacity of out.

Returns:

Samples written (coherent_bins * code_bins), or 0 at coherent_bins == 1, while a block is partial, or when out is too small.

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(PN(poly=mls_poly(5), seed=1,
...                      length=5).generate(31)).astype(np.uint8)
>>> s0 = np.repeat(np.where(code & 1, -1.0, 1.0), 4).astype(
...     np.complex64)
>>> b = Acquisition(code, spc=4, chip_rate=1e6, cn0_dbhz=60.0,
...                 doppler_uncertainty=40e3, code_only_epochs=7)
>>> blk = np.tile(np.roll(s0, 17), b.coherent_bins).astype(np.complex64)
>>> _ = b.push(blk)
>>> raw = np.empty(b.coherent_bins * b.code_bins, dtype=np.complex64)
>>> b.block_raw(raw) == raw.size
True
>>> bool(np.array_equal(raw, blk))      # the samples as pushed
True


function dp_acq_build_handoff

Convert one dp_acq_push() hit into a wire-ready hand-off record.

void dp_acq_build_handoff (
    const dp_acq_state_t * state,
    const acq_result_t * hit,
    size_t code_len,
    size_t spc,
    acq_handoff_t * out
) 

Two convention inversions live here, ported verbatim from dsss_receiver_core.c's own (pre-existing, now-shared) handoff logic:

  • Chip phase: hit's code_phase is a correlation LAG (0 … code_bins-1); a code-tracking loop's init_chip wants the code's own instantaneous phase instead — the mirror-image inversion phase = fmod(code_len - code_phase/spc, code_len), folded non-negative.
  • Doppler: state is assumed built via dp_acq_create_continuous() (window_bins tiles of coherent_bins each, the only mode this function supports), so hit's doppler_bin indexes the uniform grid of window_bins * coherent_bins bins, mapped to a signed bin by dp_fftfreq_index() — the SAME helper the search uses — and scaled by state->doppler_res_hz.
  • The dwell's dilation (doppler#1254): a hit is decided on a non-coherent sum over n_noncoh looks, and the code phase it reports is that sum's peak the phase at the MIDDLE of the dwell, not at its end, when the chip clock is dilated by the same Doppler the hit reports (a physically-coupled carrier, doppler_hz / carrier_freq_hz chips per chip). The seed a code loop wants is the phase at the next sample, so with the carrier set (dp_acq_set_carrier_freq_hz()) the phase is advanced by the drift over HALF the dwell, doppler_hz_est / carrier_freq_hz * n_noncoh * coherent_bins * code_len / 2 chips (a coherent block's epochs are aligned to its middle by the same setting, so the block's peak is its middle too). At SPEC's 20 ppm the continuous engine's dwell at 45 dB-Hz is 15 epochs (0.15 chip, inside any code loop's pull-in) and at the 40 dB-Hz floor 88 epochs 0.9 chip, measured directly, past the refine Dll's; without this the floor's hand-offs never refined. Uncoupled (0.0): no advance.

Parameters:

  • state The engine hit came from (non-NULL, built via dp_acq_create_continuous()).
  • hit One hit from dp_acq_push() (non-NULL).
  • code_len Spreading-code length (chips) — the same value passed to whichever acq_create_*() built state.
  • spc Samples/chip — likewise.
  • out Written on return (non-NULL).

function dp_acq_carrier_freq_ok

Whether carrier_freq_hz is a carrier the code can be coupled to atsample_rate_hz: 0 (uncoupled), or finite and above half the sample rate.

int dp_acq_carrier_freq_ok (
    double carrier_freq_hz,
    double sample_rate_hz
) 

Every coupled stage divides a Doppler by the carrier: the hand-off's dilation, a coherent block's epoch alignment, a code loop's rate aid. A seed a receiver accepts has a Doppler below half its sample rate (dp_async_dsss_receiver_seed_ok()), so a carrier above that keeps |doppler / carrier| < 1, the physical v/c, when the hand-off is taken from a seed. The acquisition search itself does not yet bound the Doppler it reports to that range (doppler#2112 item 7); a hit past it is refused as a seed rather than used, so the bound holds at the receiver even where the search does not enforce it. A tiny carrier sent doppler / carrier to inf, so the seed phase came out NaN, and the receiver aborted building its code loop from it (doppler#2103). The one rule for every site that takes the divisor: dp_acq_set_carrier_freq_hz() and the DSSS receivers' creates.

Returns:

1 if the carrier is taken, 0 otherwise (a NaN is not).


function dp_acq_cell_corr

One epoch's correlation against the replica at ONE code phase and ONE frequency: the engine's surface, evaluated at a single cell, on raw samples.

double _Complex dp_acq_cell_corr (
    const dp_acq_state_t * state,
    const float _Complex * x,
    size_t col,
    double f_hz,
    double t0
) 

sum_m x[m] * conj(ref[(m - col) mod nx]) * exp(-j 2 pi f (t0 + m)/fs) over the code_bins samples of x. The mixer phase is referenced to t0, so epochs evaluated at consecutive t0 combine COHERENTLY: sum the returned values across epochs and the result is a depth-len cell of the slow-time surface at f_hz, with no intra-epoch rotation loss.

It is what a caller does once acquisition has SETTLED the code phase and the Doppler: evaluate the same statistic elsewhere without searching the grid again. The engine's tile-alias check uses it; burst_capture's refine uses it to score each candidate repetition (doppler#1502).

Parameters:

  • state The engine whose replica, code_bins and fs are used.
  • x One epoch, code_bins samples.
  • col Code phase, samples, in [0, code_bins).
  • f_hz Mixer frequency, Hz.
  • t0 Time of the first sample of x, in samples, on the caller's reference.

Returns:

The complex correlation.


function dp_acq_cell_corr_grid

dp_acq_cell_corr() at many frequencies over consecutive epochs, in one pass: the cells a caller scores once the code phase is settled and only the Doppler is left to search.

void dp_acq_cell_corr_grid (
    const dp_acq_state_t * state,
    const float _Complex * x,
    size_t n_epochs,
    size_t col,
    const double * f_hz,
    size_t n_f,
    double t0,
    double _Complex * out
) 

Element e * n_f + j of out is dp_acq_cell_corr() of epoch e (x + e*code_bins, at t0 + e*code_bins) at the j-th frequency, to within 1e-5 of the epoch's sum |x * ref|, the most either can reach (measured 2.9e-6 at a 510-sample epoch): a coherent peak 100 dB above the difference.

How: evaluated one cell at a time, every frequency repeats the whole despread-and-mix of the epoch. Here the replica is multiplied once per call by a mixer at the frequencies' centre, so each sample costs one multiply, summed over B equal blocks per epoch with the first and second moments of the time within each block. What is left of a frequency is a residual of at most r cycles per epoch either side of the centre; B = ceil(48 r) blocks keep it turning under 1/96 of a cycle either side of a block's centre, and it is applied per block from the moments as a second-order expansion, whose remainder is the error above. So the cost per epoch is code_bins + 3 * B * n_f against n_f * code_bins, and burst_capture's refine, which scores every Doppler cell of every candidate period, is the reason it exists (doppler#1538).

Frequencies spread more than one cycle per epoch either side of their centre fall back to dp_acq_cell_corr() per cell.

Parameters:

  • state The engine whose replica, code_bins and fs are used.
  • x n_epochs * code_bins contiguous samples.
  • n_epochs Epochs to evaluate.
  • col Code phase, samples, in [0, code_bins).
  • f_hz n_f mixer frequencies, Hz, in any order.
  • n_f Number of frequencies.
  • t0 Time of the first sample of x, in samples, on the caller's reference.
  • out Written with n_epochs * n_f correlations, epoch-major.

function dp_acq_configure_search_raw

Pin the search grid directly, bypassing both auto-sizing searches — the advanced escape hatch (mirrors Dll's/Costas's configure_lock_raw()).

int dp_acq_configure_search_raw (
    dp_acq_state_t * state,
    size_t doppler_bins,
    size_t n_noncoh
) 

Resizes every buffer/plan that depends on the grid (the slow-time FFT, the code correlator, the reference, and every per-frame scratch buffer), re-derives the threshold ladder for the pinned grid from the same physics dp_acq_create_burst()/dp_acq_create_continuous() used, and clears in-flight accumulation (ring contents, the non-coherent power accumulator, dwell bookkeeping) — call between push() calls, never a substitute for one.

Parameters:

  • state Allocated engine (non-NULL).
  • doppler_bins Coherent depth to pin, in [1, reps].
  • n_noncoh Non-coherent look count to pin, in [1, ACQ_N_NONCOH_SAFETY_CEILING].

Returns:

0 on success, -1 if either argument is out of range (or n_noncoh > 1 on an engine with rate hypotheses, which do not compose with non-coherent looks), or an allocation fails (the engine is left usable at its prior grid on failure).

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(PN(poly=mls_poly(5), seed=1,
...                      length=5).generate(31)).astype(np.uint8)
>>> s0 = np.repeat(np.where(code & 1, -1.0, 1.0), 4).astype(
...     np.complex64)
>>> a = Acquisition(code, spc=4, chip_rate=1e6, cn0_dbhz=50.0)
>>> a.configure_search_raw(doppler_bins=1, n_noncoh=4)  # pin the grid
>>> a.doppler_bins, a.n_noncoh
(1, 4)
>>> burst = np.tile(np.roll(s0, 17), 4).astype(np.complex64)
>>> a.push(burst)[0][:2]      # detects at the pinned grid
(0, 17)


function dp_acq_consumed

Input samples the last dp_acq_push() took.

size_t dp_acq_consumed (
    const dp_acq_state_t * state
) 

Per call: equal to its n_in unless the push stopped result full, or too few slots left for the next frame that ends a dwell and then the caller resumes at x + consumed. Not acq_result_t::samples_consumed, which is CUMULATIVE and counts only framed samples the stream position a hit's epoch ended at so resuming from it would re-feed the carry and double-feed the stream. 0 after create, reset, a regrid and set_state.

Parameters:

  • state Must be non-NULL.

function dp_acq_create_burst

Create a burst-mode acquisition engine for a repeated preamble: coherent multi-repetition combining, up to reps deep.

dp_acq_state_t * dp_acq_create_burst (
    const float _Complex * tmpl,
    size_t n,
    size_t reps,
    double fs,
    double cn0_dbhz,
    double doppler_uncertainty,
    double pfa,
    double pd,
    int noise_mode,
    double doppler_rate,
    size_t rate_hypotheses
) 

Takes the preamble as its SAMPLES: one period of n complex samples at fs, repeated up to reps times. A PN code is one such preamble pass its chips mapped by dp_bin_to_nrz() and held spc samples each, at fs = chip_rate * spc. The engine sees one chip = one sample: sf = n, spc = 1, chip_rate = fs, the native Doppler span is +/- fs/(2n), and code_phase is the delay into the repetition, in samples for a preamble whose ambiguity function is a ridge (Zadoff-Chu: one native bin of Doppler moves the peak u^-1 mod n samples), the delay plus that shift whenever the Doppler is not resolved before correlation. It converts cn0_dbhz to a per-sample amplitude SNR (snr = sqrt(10^(cn0_dbhz/10) / fs)), and picks the smallest coherent depth coherent_bins in [1, reps] whose burst Pd pd_burst meets pd at the Sidak threshold (minimum latency for a strong signal). pd_burst treats the preamble as exactly reps periods at a uniform offset against the stream-aligned dwells, and credits every dwell it spans (doppler#1498). If no depth meets pd the engine takes the one with the most burst Pd and is underpowered; it does NOT add non-coherent looks. A burst has one frame of preamble, so looks beyond it add noise to the statistic and move the hit samples_consumed is stamped at the end of the LAST accumulated look, so a consumer resolving the preamble's position sees an anchor up to n_noncoh*coherent_bins periods late (doppler#1181). Intended for an unmodulated burst or preamble window a continuous, data-modulated signal should use dp_acq_create_continuous() instead (coherent combining under continuous data is a structural aliasing mislock, not a tunable SNR trade-off see the file doc comment).

cn0_dbhz is the DESIGN (minimum) C/N0 and is optional: NaN (ACQ_CN0_NONE) means none was given, and the engine then integrates the whole preamble (coherent_bins = reps; 1 in a tiled search, where a depth multiplies the buffers and the latency and only a stated C/N0 asks for one) with the threshold set by pfa alone. pd is a sizing target only when a design C/N0 is given; without one pd_predicted and pd_burst are NAN and underpowered is never set.

A tighter doppler_uncertainty narrows the scanned Doppler band, lowering the per-cell threshold (more sensitive). When doppler_uncertainty exceeds the native span fs/(2n), engages the wideband window-tiling mechanism (see the file doc comment): 2*ceil((du - span)/(2*span)) + 1 windows, with a coherent depth chosen inside each (1 with no design C/N0). Use dp_acq_configure_search_raw() to pin the grid directly instead of relying on this auto-sizer.

What a code once knew analytically, the preamble's own correlation gives numerically, from one FFT at construction (acq_shape_t):

  • the peak zone (the twin rule's reach and the exclusion around each listed peak) is the autocorrelation's first null, the lag of the first local minimum of |R(k)| one chip of samples for a code;
  • the delay straddle the Pd model averages is the band-limited autocorrelation over a half-sample offset.

For a PN code the numeric model is the more accurate one: against a rectangular-chip signal it stays conservative (never optimistic) and sits 0.01-0.08 below the measured Pd at spc 2-4, where the analytic triangle sat 0.03-0.21 below (doppler#1470).

The within-repetition rotation loss keeps the engine's sinc model: it is the zero-delay cut of the ambiguity function, which a periodic preamble's envelope does not move measured within 0.007 dB of sinc for RRC-shaped QPSK at 4.7 dB peak-to-average.

tmpl is scaled to unit RMS (a copy the caller's buffer is only read), so peak_mag and noise_est come out in the signal's units whatever the preamble's amplitude. C/N0 is the preamble's mean power.

What hypotheses change, when rate_hypotheses is n > 1: * Depth. The drift bound is applied to the RESIDUAL rate, the farthest a true rate can sit from a hypothesis, R/(n - 1), so coherent_bins can grow by up to sqrt(n - 1) at a fixed span. * False alarms. A dwell now offers n looks at each cell: the per-cell Pfa is split over n times the cells the dwell's maximum ranges over (every searched row of the Doppler-interpolated surface, times code_bins), and the gate is the larger count's. Each hypothesis is CFAR-tested against its own surface and the dwell takes the strongest; acq_result_t:: doppler_rate_hz_s names the winner. * Cost. A dwell does n slow-time passes (and a recompute of the winner when it was not the last): about n times the per-dwell cost. * Pd. The model prices a rate ON a hypothesis. Between two, the coherent gain falls with the residual; the depth bound limits that to half a slow-time row, and validate_acq_rate_pd measures it (a rate half a spacing from every hypothesis, the worst case, against a rate drawn uniformly).

The count is config, not running state, and it is a reject key of the state blob: a blob from an engine with another count is refused, not reinterpreted.

Parameters:

  • tmpl One period of the preamble, n samples; not all zero, every sample finite.
  • n Samples per repetition (>= 1).
  • reps Max coherent repetitions, the coherence ceiling (>= 1).
  • fs Sample rate in Hz (> 0); 1 for normalized units.
  • cn0_dbhz Design carrier-to-noise density in dB-Hz, of the preamble's mean power: any finite value (at fs = 1 the per-sample SNR, usually negative), or ACQ_CN0_NONE (NaN) for no design point size for the whole preamble.
  • doppler_uncertainty One-sided Doppler search half-range in Hz; 0 uses the full native span +/- fs/(2n). A value greater than the native span engages wideband mode (see the file doc comment above): the uncertainty is tiled with parallel frequency-window hypotheses and a coherent depth is chosen inside every tile (1 with no design C/N0); the surface is window_bins * coherent_bins rows.
  • pfa Target system (max-of-N) false-alarm probability (0,1).
  • pd Target detection probability (0,1); a sizing target only when cn0_dbhz is given.
  • noise_mode CFAR mode index: 0=mean, 1=median, 2=min, 3=max.
  • doppler_rate Doppler rate in Hz/s (>= 0) the coherent depth is bounded against: at most f_epoch/sqrt(2*doppler_rate) repetitions (f_epoch = fs/n, the repetition rate), so the carrier's drift over one block stays inside half a slow-time row (doppler#1482). 0 is no bound: the depth is sized up to reps. With rate_hypotheses > 1 it is the SPAN of the rates searched, below.
  • rate_hypotheses Doppler-rate hypotheses to search (0, or >= 1 and at most ACQ_RATE_HYPS_MAX); 1 searches none and doppler_rate only caps the depth. Above 1 the engine COMPENSATES the rate: doppler_rate is the span [-R, +R] and the coherent block is searched at rate_hypotheses rates evenly spaced across it, a_h = -R + 2 R h / (n - 1). For each the slow-time axis is multiplied by exp(-j pi a_h t^2), t the epoch's time from the block's middle, before the slow-time transform so the Doppler an engine reports is the carrier's at the block's middle, where a hand-off wants it, and a signal whose rate sits within half a spacing of a hypothesis integrates as a still one (see below). A block of one or two epochs cannot see a rate every hypothesis is then the same surface so an engine sized to D <= 2 searches ONE look (the gate of an engine without hypotheses), runs once, and reports a rate of 0. Needs a non-zero doppler_rate: it is the span. 0 is AUTO: the engine picks the smallest n whose residual R/(n - 1) no longer limits the depth the sizer would choose with no drift bound at all (priced at that n), or ACQ_RATE_HYPS_MAX if none does; the choice is the engine's rate_hyps. Burst only, and needs a non-zero doppler_rate like any n > 1.

Returns:

Heap-allocated state, or NULL on bad arguments (including a rate_hypotheses above ACQ_RATE_HYPS_MAX, or 0 or above 1 with doppler_rate == 0) / allocation failure.

// 127-sample Zadoff-Chu preamble at 1 MS/s, up to 8 repetitions
float _Complex zc[127];
for (int k = 0; k < 127; k++)
  zc[k] = cexpf (-I * (float)(M_PI * 5.0 * k * (k + 1) / 127.0));
dp_acq_state_t *a = dp_acq_create_burst (zc, 127, 8, 1.0e6, 50.0, 0.0, 1e-3,
                                   0.9, 0, 0.0, 1);
dp_acq_destroy (a);


function dp_acq_create_continuous

Create a continuous-mode acquisition engine: always wideband window-tiling, allowing a block-coherent depth inside the tiles to accommodate waveforms with code-only windows.

dp_acq_state_t * dp_acq_create_continuous (
    const uint8_t * code,
    size_t code_len,
    size_t spc,
    double chip_rate,
    double symbol_rate,
    double cn0_dbhz,
    double doppler_uncertainty,
    double pfa,
    double pd,
    int noise_mode,
    size_t code_only_epochs,
    double doppler_rate
) 

Builds the single-row oversampled BPSK reference from code, infers sf = code_len, converts cn0_dbhz to a per-sample amplitude SNR, and ALWAYS tiles window_bins = 2*ceil((doppler_uncertainty - span) / (2*span)) + 1 parallel frequency-window hypotheses, with span = chip_rate/(2*sf) one native span (see the file doc comment's "Wideband window-tiling mode") unconditionally, so window_bins = 1 when doppler_uncertainty is no wider than one native span. A continuous, data-modulated signal's own bit transitions make coherent multi-epoch combining across DATA a structural aliasing mislock (see docs/design/dsss-acquisition.md), so the depth is bounded by what a waveform's code-only window holds: coherent_bins = D = min((code_only_epochs+1)/2, f_epoch/sqrt(2*doppler_rate)), at least 1, rounded down to a 7-smooth length (dp_fft_smooth_floor(): 94 runs as 90), run in non-overlapping D-epoch blocks inside every tile (file doc, design §2.3). With code_only_epochs = 1 it is 1 and the engine is exactly the epoch-by-epoch search. Sensitivity margin beyond the depth comes from auto-selected non-coherent looks over blocks (up to the internal ACQ_N_NONCOH_SAFETY_CEILING).

Parameters:

  • code PN chips (0/1), length code_len.
  • code_len Number of chips supplied (= sf, the spreading factor).
  • spc Samples per chip (>= 1).
  • chip_rate Chip rate in Hz (> 0).
  • symbol_rate Continuous data-symbol rate in Hz; <= 0 means no known clock. Diagnostic only (exposed via dp_acq_state_t::epochs_per_symbol), doesn't feed sizing: this engine never coherently combines regardless of the data-modulation clock.
  • cn0_dbhz Carrier-to-noise density in dB-Hz: any finite value. A continuous engine needs one its non-coherent looks cannot be chosen without a target.
  • doppler_uncertainty One-sided Doppler search half-range in Hz; 0 uses the full native span +/- chip_rate/(2*sf) (still window-tiled, at window_bins=1).
  • pfa Target system (max-of-N) false-alarm probability (0,1).
  • pd Target detection probability (0,1).
  • noise_mode CFAR mode index: 0=mean, 1=median, 2=min, 3=max.
  • code_only_epochs Whole code-only epochs a waveform's code-only window holds at any chip phase (design §2.1: floor(W_symbols * chips_per_symbol / sf) - 1); 1 (>= 1) means no window and a depth of 1.
  • doppler_rate Doppler rate in Hz/s the depth is bounded against (>= 0); 0 leaves the window as the only bound.

Returns:

Heap-allocated state, or NULL on bad arguments / allocation failure.

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(PN(poly=mls_poly(5), seed=1,
...                      length=5).generate(31)).astype(np.uint8)
>>> s0 = np.repeat(np.where(code & 1, -1.0, 1.0), 4).astype(
...     np.complex64)
>>> burst = np.tile(np.roll(s0, 17), 23).astype(np.complex64)
>>> a = Acquisition(code, spc=4, chip_rate=1e6, cn0_dbhz=50.0)
>>> a.push(burst)[0][:2]    # detects (Doppler-window bin, code phase)
(0, 17)
>>> a.coherent_bins            # no window given: one epoch
1
>>> b = Acquisition(code, spc=4, chip_rate=1e6, cn0_dbhz=50.0,
...                 code_only_epochs=7)
>>> b.coherent_bins            # (7 + 1) // 2: a whole block fits
4


function dp_acq_destroy

Destroy and free an engine.

void dp_acq_destroy (
    dp_acq_state_t * state
) 

Parameters:

  • state May be NULL.

function dp_acq_dsl_db

The search's Doppler-offset sidelobe level, dB: 20*log10 of the largest lag of the code's periodic ambiguity outside the mainlobe, at any Doppler offset the grid's tiles can put between an emitter and a tile, relative to the emitter's own peak ( acq_shape_t::dsl ).

double dp_acq_dsl_db (
    dp_acq_state_t * state
) 

What it predicts: one emitter's peak list carries peaks up to this far below its own, more than a mainlobe from its own lag, in the tiles it is not in and they stay there dwell after dwell, being the code's, not the noise's. A list consumer that skips the strongest peak (a rank, an exclusion) must not take one for a second emitter. It is at least dp_acq_psl_db(), which is its zero-offset case, and equals it on a single-tile grid searched at one Doppler. -16.95 dB for the receivers' Gold-1023 search at two samples a chip over +-5 kHz (five 3 kHz tiles), against a -23.9 dB psl. Configuration, not state: fixed at construction, kept by a grid pinned later, and not serialized.

#include "doppler/acq/acq_core.h"
#include "doppler/gold/gold_core.h"
#include <stdio.h>

int main (void)
{
  uint8_t          code[1023];
  dp_gold_state_t *g = dp_gold_create (934, 350, 567, 73, 10);
  dp_gold_generate (g, 1023, code, 1023); // the SPEC Gold-1023 code
  dp_gold_destroy (g);
  dp_acq_state_t *a = dp_acq_create_continuous (
      code, 1023, 2, 3.069e6, 2700.0, 47.0, 5000.0, 1e-3, 0.9, 0, 1,
      0.0);
  printf ("%.1f %.1f\n", dp_acq_psl_db (a), dp_acq_dsl_db (a));
  // -23.9 -16.9
  dp_acq_destroy (a);
  return 0;
}

function dp_acq_gate_stat

The gate a dwell's statistic must clear to be a hit, in the units of acq_result_t::test_stat :threshold on the coherent path (one look),eta_nc on the non-coherent one. A dwell hits when its strongest cell's statistic exceeds this, and every listed peak's statistic does too. Both statistics are linear in the cell's amplitude, so a list consumer can compare a peak's excess over another's against it in the same units. Configuration: fixed by the thresholds the grid was sized with.

double dp_acq_gate_stat (
    const dp_acq_state_t * state
) 


function dp_acq_get_state

Serialize state's cross-call state intoblob (caller-owned,dp_acq_state_bytes() long). Call between pushes (no partial dump pending).

void dp_acq_get_state (
    const dp_acq_state_t * state,
    void * blob
) 


function dp_acq_position

The engine's stream position: every sample its pushes have taken, framed ones and the carry, since create, reset, a regrid or set_state's restore.

uint64_t dp_acq_position (
    const dp_acq_state_t * state
) 

The timebase acq_result_t::samples_consumed is counted in. Read at the start of a push, it is where that push's input begins, so a hit from that push ended at x + (hit.samples_consumed - position): the hand-off a tracker resumes from. A caller's own count of what it has fed is a different timebase configure_search_raw() and set_state() reset or restore the engine and not the caller's count and subtracting the two reads outside x.

float _Complex pre[124];               // a 31-chip code at 4x
for (size_t i = 0; i < 124; i++)
  pre[i] = (i / 4) % 3 ? 1.0f : -1.0f;
dp_acq_state_t *a = dp_acq_create_burst (pre, 124, 16, 4.0e6, 45.0,
                                         0.0, 1e-3, 0.9, 0, 0.0, 1);
float _Complex x[300] = { 0 };
uint64_t       before = dp_acq_position (a);           // 0
acq_result_t   hit;
if (dp_acq_push (a, x, 300, &hit, 1) == 1)
  printf ("hit ended at x + %llu\n",
          (unsigned long long)(hit.samples_consumed - before));
uint64_t after = dp_acq_position (a); // 300: framed and carried
dp_acq_destroy (a);
if (before != 0 || after != 300)
  return 1;

Parameters:

  • state Must be non-NULL.

function dp_acq_psl_db

The preamble's peak sidelobe level, dB: 20*log10 of the largest periodic-autocorrelation lag outside the mainlobe, relative to the peak ( acq_shape_t::psl ).

double dp_acq_psl_db (
    const dp_acq_state_t * state
) 

What it predicts: a detection's sidelobes sit this far below it, at delays outside the peak zone where the peak list's exclusion does not reach. So a burst that clears the threshold by more than -psl_db also lists its own sidelobe as a second peak (with max_peaks > 1), and a strong burst's sidelobe can mask a weak one there. A code's is its periodic floor, 20*log10(1/31) = -29.8 dB for a 31-chip m-sequence; a perfect sequence (Zadoff-Chu, a Frank-type chirp) has none and reads -INFINITY. Configuration, not state: it is fixed at construction and not serialized.

>>> import numpy as np
>>> from doppler.acquire import BurstAcquisition
>>> from doppler.cvt import bin_to_nrz
>>> from doppler.wfm import PN
>>> chips = (np.asarray(PN(poly=0, seed=1, length=5).generate(31)) & 1
...          ).astype(np.uint8)
>>> nrz = np.zeros(31, dtype=np.float32)
>>> _ = bin_to_nrz(chips, nrz)
>>> pre = np.repeat(nrz, 4).astype(np.complex64)    # 4 samples a chip
>>> a = BurstAcquisition(pre, reps=4, fs=4.0e6, cn0_dbhz=50.0)
>>> round(a.psl_db, 2)          # an m-sequence: 1/31
-29.83
>>> k = np.arange(127)
>>> zc = np.exp(-1j * np.pi * 5 * k * (k + 1) / 127).astype(np.complex64)
>>> BurstAcquisition(zc, reps=8, fs=1.0e6, cn0_dbhz=50.0).psl_db
-inf

function dp_acq_push

Stream raw samples; emit one event per CFAR dump above threshold.

size_t dp_acq_push (
    dp_acq_state_t * state,
    const float _Complex * x,
    size_t n_in,
    acq_result_t * result,
    size_t max_results
) 

Buffers x, then for every complete frame applies the slow-time Doppler FFT, correlates against the PN reference, dumps the coherent surface (or, when n_noncoh > 1, accumulates |·|² over n_noncoh looks first), gates the peak on the auto-configured threshold, and appends an acq_result_t. Each event carries the peak's Doppler bin and code phase (the two search axes), its CFAR statistic, and an estimated C/N0 — see acq_result_t.

Python's push() has room for 1024 events a call, so a push that ends at most 1024 / max_peaks dwells, counting the carry, loses nothing. Once fewer than max_peaks slots are left, the call stops before the next frame that would end a dwell and once all 1024 are used, before the next frame of any kind and the rest of its input is lost, unless the rest completes no frame, counting the carry already held: that is kept as the carry. The next push stays on the frame grid only when what was lost is a whole number of frames. Before v0.66 the room was 64, and a push past it kept up to ring_cap/frame_n - 1 frames for the next call, dropped the rest, and could cut a dwell's list short. #1992 and just-buildit/just-makeit#2184 track sizing the list to the call.

Parameters:

  • state Allocated engine (non-NULL).
  • x Raw input, interleaved CF32, n_in complex samples.
  • n_in Number of complex input samples.
  • result Output array for detection events.
  • max_results Capacity of result. A full result never loses input. A frame that ends a dwell may report up to max_peaks events, so it is taken only while at least min(max_peaks, max_results) slots are left (at least one); a frame that ends none needs no room while any slot is left. A FULL result takes nothing more, so at room 1 a push frames nothing past its hit. At the first frame it does not take, the push takes the rest of its input only if the rest completes no frame, counting the carry (it is then the carry), and otherwise stops on that frame's boundary. dp_acq_consumed() says how many samples it took, and the caller offers the rest again. A max_results under max_peaks cannot hold a whole dwell: a push with result empty still takes the dwell and keeps its strongest max_results picks. That loses RESULTS the dwell's weaker picks never input; size max_results >= max_peaks to lose none. At 0 result is full from the start: a push takes only a rest that completes no frame, so a resume loop needs room for at least one.

Returns:

Number of events written (0 … max_results).

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(PN(poly=mls_poly(5), seed=1,
...                      length=5).generate(31)).astype(np.uint8)
>>> s0 = np.repeat(np.where(code & 1, -1.0, 1.0), 4).astype(
...     np.complex64)
>>> a = Acquisition(code, spc=4, chip_rate=1e6, cn0_dbhz=50.0,
...                 doppler_uncertainty=40e3)
>>> fs = 1e6 * 4                    # sample rate = chip_rate * spc
>>> t = np.arange(a.code_bins * a.n_noncoh)
>>> carrier = np.exp(2j * np.pi * (a.doppler_res_hz / fs) * t)
>>> sig = (np.tile(np.roll(s0, 17), a.n_noncoh)
...        * carrier).astype(np.complex64)
>>> a.push(sig)[0][:2]              # (Doppler-window bin, code phase)
(1, 17)


function dp_acq_rate_hypotheses_for

The fewest rate hypotheses over [-span, +span] that leave the residualspan/(n - 1) within what a block ofdepth epochs tolerates,f_epoch^2/(2*depth^2) __ the drift rule behind the depth cap, inverted. At least 2 for a positive span; 1 for none; at mostACQ_RATE_HYPS_MAX .

size_t dp_acq_rate_hypotheses_for (
    double f_epoch,
    double span,
    size_t depth
) 

Parameters:

  • f_epoch Epoch rate, Hz (fs/n for a burst engine).
  • span Rate span, Hz/s (>= 0).
  • depth Block depth in epochs (>= 1).

function dp_acq_rate_hypothesis

Rate hypothesis h ofn over[-span, +span] , Hz/s:-span + 2*span*h/(n - 1) ; 0 forn <= 1 .

double dp_acq_rate_hypothesis (
    double span,
    size_t n,
    size_t h
) 

The engine's grid, exported so a composing object searches the SAME rates and not a restatement of them (dp_burst_capture's refine).


function dp_acq_rate_phasor

The de-chirp phasor exp(-j*pi*rate*t^2) for a rate in Hz/s at timet seconds from the block's middle.

double _Complex dp_acq_rate_phasor (
    double rate,
    double t
) 

What the engine multiplies a block's slow-time axis by, and what refine multiplies its per-period correlations by: one definition of the centring and the sign.


function dp_acq_reset

Drain the input ring and reset the coherent accumulator.

void dp_acq_reset (
    dp_acq_state_t * state
) 

Discards any buffered samples that have not yet completed a frame and clears the non-coherent power accumulator and dwell bookkeeping, so the next push() begins a fresh search from an empty ring. The construction parameters — grid, thresholds, and PN reference — are untouched; only the in-flight streaming state is dropped.

Parameters:

  • state Must be non-NULL.
    >>> import numpy as np
    >>> from doppler.acquire import Acquisition
    >>> from doppler.wfm import PN, mls_poly
    >>> code = np.asarray(PN(poly=mls_poly(5), seed=1,
    ...                      length=5).generate(31)).astype(np.uint8)
    >>> s0 = np.repeat(np.where(code & 1, -1.0, 1.0), 4).astype(
    ...     np.complex64)
    >>> burst = np.tile(np.roll(s0, 17), 23).astype(np.complex64)
    >>> a = Acquisition(code, spc=4, chip_rate=1e6, cn0_dbhz=50.0)
    >>> _ = a.push(burst[:100])   # a partial frame, buffered mid-stream
    >>> a.reset()                 # drop it before it can bias a detection
    >>> a.push(burst)[0][:2]      # (Doppler bin, code phase)
    (0, 17)
    

function dp_acq_run

Pure run: inject state_in , streamin , emit hits, exportstate_out —(state_in, input) -> (state_out, output) over an engine treated as immutable config + scratch.state_in /state_out may alias. Either may be NULL (NULL in = fresh; NULL out = discard).

size_t dp_acq_run (
    dp_acq_state_t * state,
    const void * state_in,
    void * state_out,
    const float _Complex * in,
    size_t n_in,
    acq_result_t * result,
    size_t max_results
) 

The run is one dp_acq_push(), so it stops where a push stops: once result is full, or before a frame that ends a dwell when fewer than min(max_peaks, max_results) slots are left. It may then leave part of in untaken. dp_acq_consumed(state) says how much it took; the caller calls again with state_out as the next state_in and the rest, in + consumed, and the two runs together equal one over the whole input.

Returns:

Number of events written (0 … max_results).


function dp_acq_set_carrier_freq_hz

Couple the code clock to the carrier: the chip rate dilates by doppler_hz / carrier_freq_hz , and the engine accounts for it.

int dp_acq_set_carrier_freq_hz (
    dp_acq_state_t * state,
    double carrier_freq_hz
) 

A physically-coupled Doppler moves the code as well as the carrier 100 chips/s at 20 ppm of 5 Mcps and the engine's two long integrations both smear over it (doppler#1256, #1254): * Inside a coherent block of D epochs every tile's epoch correlations are shifted along the code axis by the drift the tile's own frequency implies, f_tile / carrier chips per chip, aligned to the block's middle, before the slow-time transform (a linear phase on each epoch's product, exact to a fraction of a sample). Measured at SPEC's 20 ppm with D = 154 (3.1 chips of drift across the block): without it the block's peak is 13 dB down and 3 chips wide and the depth detects nothing at 34 dB-Hz; with it the block reads as a still one. * The hand-off (dp_acq_build_handoff()) advances the hit's code phase by the drift over half the dwell the non-coherent sum's peak is the phase at the dwell's middle, the seed is wanted at its end: 0.9 chip at the 40 dB-Hz floor, past a refine loop's pull-in.

Config, not running state: it is not in the state blob, so a resumed engine wants it set again by its holder, as at create. Default 0.0 (uncoupled) is the engine exactly as it ran without it.

Parameters:

  • state Must be non-NULL.
  • carrier_freq_hz RF carrier, Hz; 0.0 = uncoupled.

Returns:

DP_OK, or DP_ERR_INVALID for a carrier outside dp_acq_carrier_freq_ok() at this engine's sample rate.

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import Gold
>>> code = np.asarray(Gold().generate(1023)).astype(np.uint8)
>>> a = Acquisition(code, spc=2, chip_rate=5e6, symbol_rate=2700.0,
...                 cn0_dbhz=45.0, doppler_uncertainty=50e3)
>>> a.carrier_freq_hz
0.0
>>> a.set_carrier_freq_hz(2.5e9)
>>> a.carrier_freq_hz
2500000000.0


function dp_acq_set_max_peaks

How many peaks a dwell may report: the peak list's capacity.

int dp_acq_set_max_peaks (
    dp_acq_state_t * state,
    size_t n
) 

One (the default) is the classic detector the maximum of the surface, gated. More is the list of docs/design/async-dsss-receiver.md §7.1: every peak above the same gate, strongest first, each with an exclusion zone of one Doppler bin by the reference's first autocorrelation null around it (one chip for a PN code; one emitter's main lobe, so its own shoulders are not the next peak), and the two-epoch rule for a peak at an already-listed code phase a data transition inside the epoch splits one emitter into twins at its own code phase on other tiles, so such a peak is held for one dwell and listed only if it was there, at the same tile, on the previous one. Each listed peak is one acq_result_t from dp_acq_push(), all of a dwell's sharing its samples_consumed and noise_est. A held twin takes a slot of the n for that dwell but is not reported. The threshold does not change: a second peak is another draw from the same cells against the same union bound. Clears the held candidates.

Parameters:

  • state Must be non-NULL.
  • n 1 … ACQ_MAX_PEAKS.

Returns:

0, or -1 (state untouched) when n is out of range.

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> code = (np.arange(31) * 5 % 2).astype(np.uint8)
>>> a = Acquisition(code, spc=2, chip_rate=1e6, symbol_rate=1e3,
...                 cn0_dbhz=50.0, doppler_uncertainty=50e3)
>>> a.max_peaks
1
>>> a.set_max_peaks(8)
>>> a.max_peaks
8


function dp_acq_set_state

Restore cross-call state from blob intostate (replacing it).

int dp_acq_set_state (
    dp_acq_state_t * state,
    const void * blob
) 

Returns:

DP_OK, or DP_ERR_INVALID if the blob's envelope or its n/n_noncoh/max_peaks disagree with state (rebuild the engine from the matching descriptor first), or its carry is not one a run could have left, or the carry's frames and the stream position disagree. A refused blob changes nothing.


function dp_acq_set_surface_sink

Attach (or detach) a C surface sink: every decim-th decided dwell's surface, in test-statistic units, handed tofn on the pushing thread (design §2.4).

void dp_acq_set_surface_sink (
    dp_acq_state_t * state,
    acq_surface_sink_fn fn,
    void * ctx,
    uint32_t decim
) 

Sets keep_surface, so dp_acq_surface() reads the same dwell afterwards. The pointer handed to fn is the engine's and is valid only for the call: copy or write it out there. This is how a long run records the surface decimated in time without a copy per dwell it does not keep.

Parameters:

  • state Must be non-NULL.
  • fn The sink, or NULL to detach.
  • ctx Passed through to fn.
  • decim Hand over every decim-th dwell; 0 reads as 1.

function dp_acq_set_telemetry

Attach (or detach) a telemetry context and register the engine's probes on it (design §2.4).

int dp_acq_set_telemetry (
    dp_acq_state_t * state,
    dp_tlm_t * tlm,
    const char * prefix,
    uint32_t decim
) 

Registers ten probes, emitted once per DECIDED dwell (a coherent dump, or the dwell that completes n_noncoh looks) and further thinned by decim: "<prefix>.stat" (the dwell's test statistic — the strongest cell against the CFAR reference, in the units the gate is set in), "<prefix>.gate" (that gate: threshold on the coherent path, eta_nc on the non-coherent one — plotted together they show exactly where a hit fired), "<prefix>.noise" (the CFAR reference noise_est), "<prefix>.peak" (the strongest cell's raw value), "<prefix>.row" and "<prefix>.col" (its native Doppler row and code-phase column — a surface coordinate, not a physical unit; dp_acq_surface_doppler_hz() and dp_acq_surface_chip_phase() convert), "<prefix>.n_peaks" (picks in the dwell, held twins included), "<prefix>.n_held" (picks held as same-code-phase twins rather than listed, §7.1), "<prefix>.conc" (the strongest pick's concentration — see peak_conc: its main lobe's power over its whole column's, near 1 for one clean emitter even when it straddles two tiles, about 0.5 when a data transition splits it into twins two or more tiles away, lower still when a coherent block straddles data — the discriminator between one emitter's splatter and a second emitter) and "<prefix>.hit" (1 when the gate fired). Passing NULL detaches. Setup path, never hot; the context is borrowed and must outlive the attachment (SPSC rules in dp_tlm/dp_tlm_core.h).

Parameters:

  • state Must be non-NULL.
  • tlm Telemetry context to attach, or NULL to detach.
  • prefix Probe-name prefix, e.g. "acq" or "ch0.acq".
  • decim Emit every decim-th decided dwell; >= 1.

Returns:

DP_OK, or DP_ERR_INVALID when the probe table cannot take all ten probes (the attach fails whole; the engine stays detached).

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.telemetry import Telemetry
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(
...     PN(poly=mls_poly(9), seed=1, length=9).generate(511), np.uint8)
>>> a = Acquisition(code, spc=2, chip_rate=1e6, cn0_dbhz=50.0)
>>> tlm = Telemetry(1 << 12)
>>> a.set_telemetry(tlm, "acq")
>>> sorted(tlm.probe_names)[:3]
['acq.col', 'acq.conc', 'acq.gate']
>>> x = np.zeros(a.n_noncoh * 511 * 2 * 3, dtype=np.complex64)
>>> _ = a.push(x)
>>> len(tlm.read()) % 10      # ten records per decided dwell
0


function dp_acq_set_threads

Set how many threads the searcher fans its tiles across (design §2.3: a roll per thread on persistent workers).

int dp_acq_set_threads (
    dp_acq_state_t * state,
    int n
) 

A continuous engine is created with a pool of the machine's online cores when it has more than one tile; a burst engine, and a single-tile one, run serially. This sets the count: 0 auto-selects the online core count, 1 runs everything on the calling thread, n runs on n workers (the caller included). The workers are created here, once, and parked between pushes; nothing is created per push. The surface is bit-identical at every count the tiles are independent after the one forward transform and each writes its own rows so this changes the cost of a push and nothing about its result. Setup path, never hot; not while another thread is inside push().

Parameters:

  • state Must be non-NULL.
  • n Thread count; 0 = online cores, 1 = serial.

Returns:

DP_OK. The count actually running is threads.

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(
...     PN(poly=mls_poly(9), seed=1, length=9).generate(511), np.uint8)
>>> a = Acquisition(code, spc=2, chip_rate=1e6, cn0_dbhz=50.0,
...                 doppler_uncertainty=4000.0)
>>> a.threads >= 1               # a pool, sized to the machine
True
>>> a.set_threads(1)
>>> a.threads
1


function dp_acq_state_bytes

Byte size of state's blob: the envelope and the engine's position, the framer's snapshot of the carry, the non-coherent sum (n_noncoh > 1), the last dwell's twins, and a tiled block's epochs.

size_t dp_acq_state_bytes (
    const dp_acq_state_t * state
) 


function dp_acq_surface

The last decided dwell's surface, in the gate's own units.

size_t dp_acq_surface (
    dp_acq_state_t * state,
    float * out,
    size_t n_out
) 

Copies the surface the last dwell was decided on into out, row-major surface_rows (Doppler: tiles, or interpolated slow-time rows) by code_bins (code phase), every cell divided by the same CFAR reference the gate used — so a cell reads as its own test statistic, to a float rounding (the SIMD build's fast-math may take a reciprocal in this loop and a divide in the gate's), and the gate (threshold, or eta_nc on the non-coherent path) is a flat plane on a plot. The engine keeps this only while keep_surface is set (a caller sets it, or dp_acq_set_surface_sink() does): set it, push, then read. surface_at says which dwell it is; a time-decimated record is the caller reading every k-th dwell, or a sink with decim.

Parameters:

  • state Must be non-NULL.
  • out At least surface_rows * code_bins floats.
  • n_out Capacity of out.

Returns:

Cells written (surface_rows * code_bins), or 0 when no dwell has been decided with keep_surface set, or out is too small.

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(
...     PN(poly=mls_poly(9), seed=1, length=9).generate(511), np.uint8)
>>> a = Acquisition(code, spc=2, chip_rate=1e6, cn0_dbhz=50.0)
>>> a.keep_surface = 1
>>> x = np.zeros(a.n_noncoh * 511 * 2, dtype=np.complex64)
>>> _ = a.push(x)
>>> s = np.empty(a.surface_rows * a.code_bins, dtype=np.float32)
>>> a.surface(s) == s.size
True
>>> s.reshape(a.surface_rows, a.code_bins).shape == (a.surface_rows, 1022)
True


function dp_acq_surface_chip_phase

The surface's code-phase axis: the chip phase of each column.

size_t dp_acq_surface_chip_phase (
    dp_acq_state_t * state,
    double * out,
    size_t n_out
) 

One value per surface column, in chips, the same mapping dp_acq_build_handoff() applies to a hit's code_phase — so a plotted peak sits at the chip phase the DetectionEvent would carry.

Parameters:

  • state Must be non-NULL.
  • out At least code_bins doubles.
  • n_out Capacity of out.

Returns:

Values written (code_bins), or 0 if out is too small.

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(
...     PN(poly=mls_poly(9), seed=1, length=9).generate(511), np.uint8)
>>> a = Acquisition(code, spc=2, chip_rate=1e6, cn0_dbhz=50.0)
>>> c = np.empty(a.code_bins, dtype=np.float64)
>>> a.surface_chip_phase(c) == a.code_bins
True
>>> bool(c[0] == 0.0 and c[1] == 510.5)
True


function dp_acq_surface_complex

The last decided dwell's surface, complex: amplitude and carrier phase per cell, before the magnitude the gate reads.

size_t dp_acq_surface_complex (
    dp_acq_state_t * state,
    float _Complex * out,
    size_t n_out
) 

Copies the coherent sum the last dwell was decided on into out, row-major surface_rows x code_bins like dp_acq_surface(), in the correlation's own units rather than the gate's. A cell's phase is the carrier at the block's middle, relative to its tile's centre; its neighbours along the code axis are complex early and late arms, so a tracker can form the coherent discriminator Re(conj(P) (L - E)) / |P|^2, which the magnitude surface cannot (docs/design/async-dsss-receiver-measurements.md §12.21). Coherent path only: a non-coherent dwell (n_noncoh > 1) is a power sum with no phase, and reads 0.

Parameters:

  • state Must be non-NULL.
  • out At least surface_rows * code_bins complex floats.
  • n_out Capacity of out.

Returns:

Cells written (surface_rows * code_bins), or 0 when no dwell has been decided, the path is non-coherent, or out is too small.

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(PN(poly=mls_poly(5), seed=1,
...                      length=5).generate(31)).astype(np.uint8)
>>> s0 = np.repeat(np.where(code & 1, -1.0, 1.0), 4).astype(
...     np.complex64)
>>> a = Acquisition(code, spc=4, chip_rate=1e6, cn0_dbhz=70.0)
>>> a.n_noncoh                 # one look: the dwell is the coherent dump
1
>>> _ = a.push(np.roll(s0, 17).astype(np.complex64))
>>> sc = np.empty(a.surface_rows * a.code_bins, dtype=np.complex64)
>>> a.surface_complex(sc) == sc.size
True
>>> int(np.argmax(np.abs(sc)) % a.code_bins)   # the peak's code phase
17


function dp_acq_surface_doppler_hz

The surface's Doppler axis: the frequency of each row, in Hz.

size_t dp_acq_surface_doppler_hz (
    dp_acq_state_t * state,
    double * out,
    size_t n_out
) 

One value per surface row, the fold and scale a hit's doppler_hz_est uses (dp_fftfreq_index() times doppler_res_hz, on the interpolated grid where the slow-time axis is interpolated), so a plot of dp_acq_surface() carries the same axis a DetectionEvent reports on.

Parameters:

  • state Must be non-NULL.
  • out At least surface_rows doubles.
  • n_out Capacity of out.

Returns:

Values written (surface_rows), or 0 if out is too small.

>>> import numpy as np
>>> from doppler.acquire import Acquisition
>>> from doppler.wfm import PN, mls_poly
>>> code = np.asarray(
...     PN(poly=mls_poly(9), seed=1, length=9).generate(511), np.uint8)
>>> a = Acquisition(code, spc=2, chip_rate=1e6, cn0_dbhz=50.0,
...                 doppler_uncertainty=4000.0)
>>> f = np.empty(a.surface_rows, dtype=np.float64)
>>> a.surface_doppler_hz(f) == a.surface_rows
True
>>> bool(f[0] == 0.0 and f.min() < 0.0 < f.max())
True


Public Static Functions Documentation

function acq_bin_doppler_hz

A hit's signed Doppler, Hz: its doppler_bin folded over acq_grid_bins() by numpy's fftfreq convention (dp_fftfreq_index) and scaled by the grid's resolution.

static inline double acq_bin_doppler_hz (
    const dp_acq_state_t * state,
    size_t doppler_bin
) 

The one conversion from a bin to Hz; dp_acq_build_handoff() and every composing object call it rather than restating the fold.


function acq_grid_bins

The Doppler grid a hit's doppler_bin indexes:window_bins * coherent_bins bins.

static inline size_t acq_grid_bins (
    const dp_acq_state_t * state
) 

One bin in a native search; the window-tile count once a doppler_uncertainty wider than the native span is tiled; tiles times the block depth on a continuous engine with code-only epochs. Every consumer that folds a doppler_bin must fold it over THIS, not over coherent_bins: a capture that did read every tiled hit as 0 Hz (doppler#1512).


Macro Definition Documentation

define ACQ_CN0_NONE

The design C/N0 that means "none given" (doppler#1484): a burst engine then sizes for the whole preamble, its pd_predicted is NaN and it is neverunderpowered .

#define ACQ_CN0_NONE `NAN`

NaN, because every finite value is a real design point including a negative one: at fs = 1 (normalized units) the C/N0 IS the per-sample SNR in dB, which is negative wherever acquisition is hard. It used to be 0, which made exactly 0 dB unreachable and a negative per-sample SNR unstatable. Test with isnan(), never ==.


define ACQ_COL_CHUNK

#define ACQ_COL_CHUNK `32u`

Columns the block-end transform gathers per pass (design §2.3, #1243): a cache line holds 8 cf32 cells, so a chunk of 32 columns reads four lines per slow-time row of the block and writes four per surface row, where a column at a time read and wrote one line per CELL.


define ACQ_DELAY_LOSS_NODES

#define ACQ_DELAY_LOSS_NODES `4`

Quadrature nodes the Pd model averages the delay straddle over.


define ACQ_MAX_PEAKS

#define ACQ_MAX_PEAKS `64u`

The largest max_peaks dp_acq_set_max_peaks() accepts: the bindings' push result arrays (1024) are at least this many, so one dwell can always be reported whole.


define ACQ_N_NONCOH_SAFETY_CEILING

Internal safety-valve ceiling on auto-selected non-coherent looks not a public knob (no caller-facing equivalent of the retiredmax_noncoh parameter).

#define ACQ_N_NONCOH_SAFETY_CEILING `256u`

The semi-analytical Pd model both auto-sizers ascend against turns non-monotonic and unreliable past a few hundred looks (this project's own geometry found ~256 empirically see docs/design/async-dsss-receiver.md). Hitting this ceiling without meeting pd leaves dp_acq_state_t::underpowered set, same as any other infeasible operating point no separate bookkeeping needed.


define ACQ_RATE_HYPS_MAX

#define ACQ_RATE_HYPS_MAX `64u`

Most Doppler-rate hypotheses an engine searches. Each one costs a full slow-time pass per dwell, so this bounds the cost of a typo, not a design: the residual rate falls as 1/(R-1), and a few dozen hypotheses already take a rate span out to depths no preamble repeats to.


define ACQ_STATE_MAGIC

#define ACQ_STATE_MAGIC `DP_FOURCC ('A', 'C', 'Q', 'R')`

define ACQ_STATE_VERSION

#define ACQ_STATE_VERSION `6u`


The documentation for this class was generated from the following file native/inc/doppler/acq/acq_core.h