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 NANThe 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 256uInternal safety-valve ceiling on auto-selected non-coherent looks not a public knob (no caller-facing equivalent of the retired max_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_binsin[1, reps]whose BURST Pd meetspd: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 tighterdoppler_uncertaintyshrinks the searched cell count, lowering the Sidak threshold (more sensitive). Whendoppler_uncertaintyexceeds the native span, the wideband window-tiling mechanism below tiles it, and the coherent depth is chosen INSIDE every tile the smallest that meetspdat 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_uncertaintyis 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:
stateMust be non-NULL.tileTile index,0 … window_bins-1(native FFT order, the order the surface's rows are cut in).colCode-phase column,0 … code_bins-1.outAt leastcoherent_binscomplex floats.n_outCapacity ofout.
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.
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:
stateMust be non-NULL.outAt leastcoherent_bins * code_binscomplex floats.n_outCapacity ofout.
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'scode_phaseis a correlation LAG (0 … code_bins-1); a code-tracking loop'sinit_chipwants the code's own instantaneous phase instead — the mirror-image inversionphase = fmod(code_len - code_phase/spc, code_len), folded non-negative. - Doppler:
stateis assumed built via dp_acq_create_continuous() (window_binstiles ofcoherent_binseach, the only mode this function supports), sohit'sdoppler_binindexes the uniform grid ofwindow_bins * coherent_binsbins, mapped to a signed bin bydp_fftfreq_index()— the SAME helper the search uses — and scaled bystate->doppler_res_hz. - The dwell's dilation (doppler#1254): a hit is decided on a non-coherent sum over
n_noncohlooks, 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_hzchips 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 / 2chips (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:
stateThe enginehitcame from (non-NULL, built via dp_acq_create_continuous()).hitOne hit from dp_acq_push() (non-NULL).code_lenSpreading-code length (chips) — the same value passed to whichever acq_create_*() builtstate.spcSamples/chip — likewise.outWritten 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.
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:
stateThe engine whose replica,code_binsandfsare used.xOne epoch,code_binssamples.colCode phase, samples, in [0, code_bins).f_hzMixer frequency, Hz.t0Time of the first sample ofx, 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:
stateThe engine whose replica,code_binsandfsare used.xn_epochs * code_binscontiguous samples.n_epochsEpochs to evaluate.colCode phase, samples, in [0, code_bins).f_hzn_fmixer frequencies, Hz, in any order.n_fNumber of frequencies.t0Time of the first sample ofx, in samples, on the caller's reference.outWritten withn_epochs * n_fcorrelations, 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()).
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:
stateAllocated engine (non-NULL).doppler_binsCoherent depth to pin, in[1, reps].n_noncohNon-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.
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:
stateMust 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:
tmplOne period of the preamble,nsamples; not all zero, every sample finite.nSamples per repetition (>= 1).repsMax coherent repetitions, the coherence ceiling (>= 1).fsSample rate in Hz (> 0); 1 for normalized units.cn0_dbhzDesign 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_uncertaintyOne-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 iswindow_bins * coherent_binsrows.pfaTarget system (max-of-N) false-alarm probability (0,1).pdTarget detection probability (0,1); a sizing target only whencn0_dbhzis given.noise_modeCFAR mode index: 0=mean, 1=median, 2=min, 3=max.doppler_rateDoppler rate in Hz/s (>= 0) the coherent depth is bounded against: at mostf_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 toreps. Withrate_hypotheses> 1 it is the SPAN of the rates searched, below.rate_hypothesesDoppler-rate hypotheses to search (0, or >= 1 and at most ACQ_RATE_HYPS_MAX); 1 searches none anddoppler_rateonly caps the depth. Above 1 the engine COMPENSATES the rate:doppler_rateis the span[-R, +R]and the coherent block is searched atrate_hypothesesrates evenly spaced across it,a_h = -R + 2 R h / (n - 1). For each the slow-time axis is multiplied byexp(-j pi a_h t^2),tthe 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-zerodoppler_rate:it is the span. 0 is AUTO: the engine picks the smallestnwhose residualR/(n - 1)no longer limits the depth the sizer would choose with no drift bound at all (priced at thatn), or ACQ_RATE_HYPS_MAX if none does; the choice is the engine'srate_hyps. Burst only, and needs a non-zerodoppler_ratelike anyn > 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:
codePN chips (0/1), lengthcode_len.code_lenNumber of chips supplied (= sf, the spreading factor).spcSamples per chip (>= 1).chip_rateChip rate in Hz (> 0).symbol_rateContinuous 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_dbhzCarrier-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_uncertaintyOne-sided Doppler search half-range in Hz; 0 uses the full native span +/- chip_rate/(2*sf) (still window-tiled, at window_bins=1).pfaTarget system (max-of-N) false-alarm probability (0,1).pdTarget detection probability (0,1).noise_modeCFAR mode index: 0=mean, 1=median, 2=min, 3=max.code_only_epochsWhole 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_rateDoppler 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.
Parameters:
stateMay 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 ).
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.
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).
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.
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:
stateMust 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 ).
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:
stateAllocated engine (non-NULL).xRaw input, interleaved CF32,n_incomplex samples.n_inNumber of complex input samples.resultOutput array for detection events.max_resultsCapacity ofresult. A fullresultnever 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 FULLresulttakes 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. Amax_resultsunder max_peaks cannot hold a whole dwell: a push withresultempty still takes the dwell and keeps its strongest max_results picks. That loses RESULTS the dwell's weaker picks never input; sizemax_results>= max_peaks to lose none. At 0resultis 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 .
Parameters:
f_epochEpoch rate, Hz (fs/nfor a burst engine).spanRate span, Hz/s (>= 0).depthBlock 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 .
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.
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.
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:
stateMust 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.
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:
stateMust be non-NULL.carrier_freq_hzRF 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.
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:
stateMust be non-NULL.n1 … 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).
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:
stateMust be non-NULL.fnThe sink, or NULL to detach.ctxPassed through tofn.decimHand 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:
stateMust be non-NULL.tlmTelemetry context to attach, or NULL to detach.prefixProbe-name prefix, e.g. "acq" or "ch0.acq".decimEmit 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).
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:
stateMust be non-NULL.nThread 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.
function dp_acq_surface¶
The last decided dwell's surface, in the gate's own units.
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:
stateMust be non-NULL.outAt leastsurface_rows * code_binsfloats.n_outCapacity ofout.
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.
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:
stateMust be non-NULL.outAt leastcode_binsdoubles.n_outCapacity ofout.
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.
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:
stateMust be non-NULL.outAt leastsurface_rows * code_binscomplex floats.n_outCapacity ofout.
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.
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:
stateMust be non-NULL.outAt leastsurface_rowsdoubles.n_outCapacity ofout.
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.
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.
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 .
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¶
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¶
Quadrature nodes the Pd model averages the delay straddle over.
define ACQ_MAX_PEAKS¶
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).
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¶
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_VERSION¶
The documentation for this class was generated from the following file native/inc/doppler/acq/acq_core.h