File carrier_acq_core.h¶
FileList > carrier_acq > carrier_acq_core.h
Go to the source code of this file
CarrierAcquisition — PSDMF residual-carrier frequency refinement. More...
#include "doppler/clib_common.h"#include "doppler/dp_state.h"#include "doppler/jm_perf.h"#include "doppler/psd/psd_core.h"#include "doppler/detector/detector_core.h"#include "doppler/detection/detection_core.h"#include "doppler/spectral/spectral_core.h"#include "doppler/corr/corr_core.h"#include "doppler/fft/fft_core.h"#include "doppler/acc_trace/acc_trace_core.h"
Classes¶
| Type | Name |
|---|---|
| struct | dp_carrier_acq_state_t CarrierAcquisition state. |
Public Functions¶
| Type | Name |
|---|---|
| dp_carrier_acq_state_t * | dp_carrier_acq_create (double sample_rate_hz, double symbol_rate_hz, double resolution_hz, size_t zero_pad, int window, float beta, const float * psd_template, size_t psd_template_len, double pfa, double pd, double design_snr, bool sequential, size_t max_n_blocks) Create a carrier_acq instance. |
| void | dp_carrier_acq_destroy (dp_carrier_acq_state_t * state) Destroy a carrier_acq instance and release all memory. |
| void | dp_carrier_acq_get_state (const dp_carrier_acq_state_t * state, void * blob) |
| void | dp_carrier_acq_reset (dp_carrier_acq_state_t * state) Reset to the post-create state: discard the running PSD average and detection state; n_blocks/ready/residual_hz return to their initial values. Config (psd/detector/dwell_target) is untouched. |
| int | dp_carrier_acq_set_state (dp_carrier_acq_state_t * state, const void * blob) |
| size_t | dp_carrier_acq_state_bytes (const dp_carrier_acq_state_t * state) |
| void | dp_carrier_acq_steps (dp_carrier_acq_state_t * state, const float _Complex * x, size_t x_len) Fold raw complex samples into the running PSD average and test for a detection. Accepts any chunk size across repeated calls a partial trailing block is carried to the next call. A no-op once ready is true or the give-up cap (max_n_blocks in sequential mode, dwell_target otherwise) has been reached. |
Macros¶
| Type | Name |
|---|---|
| define | CARRIER_ACQ_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc)('C', 'A', 'Q', 'R') |
| define | CARRIER_ACQ_STATE_VERSION 1u |
Detailed Description¶
Runs AFTER Acquisition's own coarse Doppler search, as a one-shot matched-filter refinement stage: feed the already-despread symbol-rate stream via steps(), and once enough non-coherent looks have accumulated to cross a Pfa/Pd-driven detection threshold, ready becomes true and residual_hz holds the sub-bin-refined residual carrier estimate, Hz.
Composes existing primitives rather than reimplementing them:
- dp_psd_state_t: FFT + window + zero-pad + non-coherent power averaging (Welch's method) the entire "measure the average power spectrum of what's coming in" half of the algorithm.
- dp_detector_state_t: FFT-based circular correlation of the averaged power spectrum against a known template (the average PSD shape of a random rectangular-pulse BPSK symbol stream by default, or a caller-supplied override for a different pulse/modulation) plus a noise-referenced test statistic and argmax lag.
- detection_core's dp_det_n_noncoh()/dp_det_threshold(): det_n_noncoh (the same chi-square statistic Acquisition's own auto-config uses) drives the precomputed fixed-dwell/give-up cap; det_threshold (sqrt(-2*ln(pfa))) is reused as the tail-quantile stand-in inside the per-block CFAR ratio threshold below.
The per-block CFAR ratio threshold is NOT dp_det_threshold_noncoherent() (that statistic a classic complex-correlator peak/noise envelope ratio does not transfer to this object's real statistic, a power-spectrum-vs-known-template correlation; confirmed via Monte Carlo, ~5x too conservative). carrier_acq_ratio_threshold() (carrier_acq_core.c) instead uses the derived H0 model for this specific statistic (an exact Gamma-sum mean/variance for the averaged, template-correlated periodogram) plus ONE empirically-calibrated tail-inflation constant (kappa, standing in for the argmax-over-nfft-correlated-lags extreme- value quantile a full closed form hasn't cleanly reduced to yet see FINISHING_PLAN.md's CarrierAcquisition section / the derive_carrier_acq_statistic.py derivation for the full story, and revisit/refine kappa when time allows).
Only the default template generator (a sinc^2 shape, DC-centred to match dp_psd_power_twosided()'s own bin order) and the 3-point parabolic sub-bin peak refinement (read directly off dp_detector_state_t's own out_buf no second correlation pass needed) are new leaf code.
Lifecycle: create -> steps()* -> (ready ? residual_hz : keep feeding) -> reset()/destroy
dp_carrier_acq_state_t *ca = dp_carrier_acq_create(
4.092e6, 100e3, 0.0, 4, DP_PSD_WINDOW_HANN, 0.0f, NULL, 0, 1e-3, 0.9,
2.0, true, 100000);
while (!ca->ready && ca->n_blocks < ca->max_n_blocks)
dp_carrier_acq_steps(ca, block, block_len);
double hz = ca->residual_hz; // valid only when ca->ready
dp_carrier_acq_destroy(ca);
Public Functions Documentation¶
function dp_carrier_acq_create¶
Create a carrier_acq instance.
dp_carrier_acq_state_t * dp_carrier_acq_create (
double sample_rate_hz,
double symbol_rate_hz,
double resolution_hz,
size_t zero_pad,
int window,
float beta,
const float * psd_template,
size_t psd_template_len,
double pfa,
double pd,
double design_snr,
bool sequential,
size_t max_n_blocks
)
In C the window is DP_PSD_WINDOW_HANN, DP_PSD_WINDOW_KAISER or DP_PSD_WINDOW_BLACKMAN_HARRIS; any other, the rectangular window included, returns NULL.
Parameters:
sample_rate_hzSample rate of the input stream, Hz (required).symbol_rate_hzSymbol rate, Hz builds the default template (required).resolution_hzDesired FFT frequency resolution, Hz. <= 0.0 is a sentinel meaning "auto": symbol_rate_hz/10.0. The block is round(sample_rate_hz/resolution_hz) samples, floored at 3 the shortest whose default Hann window has any gain.zero_padPSD zero-pad factor (>= 1); see psd_core.h.window"hann" (the default, and the window the threshold was fit at), "kaiser" or "blackman-harris".betaKaiser beta (ignored for hann/blackman-harris).psd_templateKnown PSD-shape template override, length must equal nfft = next_pow_two(block * zero_pad), block as above; NULL/length-0 means "not supplied" the default rectangular-pulse sinc^2 template (from symbol_rate_hz) is used.psd_template_lenLength ofpsd_template(0 if not supplied).pfaTarget per-test false-alarm probability.pdTarget detection probability.design_snrAssumed per-sample amplitude SNR used ONLY to precompute dwell_target via dp_det_n_noncoh(); not a live measurement. An optimistic guess only affects NON-sequential mode (which trusts this one-shot wait count outright) sequential mode's own give-up bound is max_n_blocks, not dwell_target, precisely so a wrong design_snr can't stop it from trying more blocks once real data shows it needs to.sequentialTrue: test for a detection after EVERY block (the per-block CFAR ratio threshold see carrier_acq_ratio_threshold() in carrier_acq_core.c tightens as more looks accumulate), stopping the moment one fires or max_n_blocks is reached. False: accumulate silently and test once, at dwell_target.max_n_blocksSequential mode's own give-up cap (ignored by non-sequential mode, which stops at dwell_target instead) deliberately a SEPARATE, generous bound from dwell_target; capping sequential mode at design_snr's own point estimate would defeat the reason to test every block in the first place.
Returns:
Heap-allocated state, or NULL on invalid argument or allocation failure.
Note:
Caller must call dp_carrier_acq_destroy() when done.
>>> import numpy as np
>>> from doppler.acquire import CarrierAcquisition
>>> rng = np.random.default_rng(12345)
>>> bits = np.where(rng.integers(0, 2, 4000), 1.0, -1.0)
>>> data = np.repeat(bits, 8) # 8 samples/symbol BPSK
>>> t = np.arange(len(data))
>>> x = (data * np.exp(2j * np.pi * 123.0 * t / 8000.0)).astype(
... np.complex64) # residual carrier at 123 Hz
>>> ca = CarrierAcquisition(
... sample_rate_hz=8000.0, symbol_rate_hz=1000.0,
... psd_template=np.array([], dtype=np.float32))
>>> ca.steps(x) # fold the stream, testing each block
>>> ca.ready # detection fired
True
>>> round(ca.residual_hz, 0) # recovered residual carrier, Hz
123.0
function dp_carrier_acq_destroy¶
Destroy a carrier_acq instance and release all memory.
Parameters:
stateMay be NULL.
function dp_carrier_acq_get_state¶
function dp_carrier_acq_reset¶
Reset to the post-create state: discard the running PSD average and detection state; n_blocks/ready/residual_hz return to their initial values. Config (psd/detector/dwell_target) is untouched.
Use it to reuse one detector across successive captures: after a detection (or a give-up) the running average and counters are cleared, so the next steps() starts folding a fresh stream from zero.
Parameters:
stateMust be non-NULL.>>> import numpy as np >>> from doppler.acquire import CarrierAcquisition >>> ca = CarrierAcquisition( ... sample_rate_hz=8000.0, symbol_rate_hz=1000.0, ... psd_template=np.array([], dtype=np.float32)) >>> ca.steps(np.zeros(2048, dtype=np.complex64)) # accumulate looks >>> ca.n_blocks > 0 True >>> ca.reset() # discard the running PSD average >>> ca.n_blocks 0
function dp_carrier_acq_set_state¶
function dp_carrier_acq_state_bytes¶
function dp_carrier_acq_steps¶
Fold raw complex samples into the running PSD average and test for a detection. Accepts any chunk size across repeated calls a partial trailing block is carried to the next call. A no-op once ready is true or the give-up cap (max_n_blocks in sequential mode, dwell_target otherwise) has been reached.
void dp_carrier_acq_steps (
dp_carrier_acq_state_t * state,
const float _Complex * x,
size_t x_len
)
Parameters:
stateMust be non-NULL.xRaw complex input samples (cf32).x_lenNumber of samples inx.>>> import numpy as np >>> from doppler.acquire import CarrierAcquisition >>> rng = np.random.default_rng(12345) >>> bits = np.where(rng.integers(0, 2, 4000), 1.0, -1.0) >>> data = np.repeat(bits, 8) # 8 samples/symbol BPSK >>> t = np.arange(len(data)) >>> x = (data * np.exp(2j * np.pi * 123.0 * t / 8000.0)).astype( ... np.complex64) # residual carrier at 123 Hz >>> ca = CarrierAcquisition( ... sample_rate_hz=8000.0, symbol_rate_hz=1000.0, ... psd_template=np.array([], dtype=np.float32)) >>> ca.steps(x) # fold the stream, testing each block >>> ca.ready True >>> round(ca.residual_hz, 0) # recovered residual carrier, Hz 123.0
Macro Definition Documentation¶
define CARRIER_ACQ_STATE_MAGIC¶
define CARRIER_ACQ_STATE_VERSION¶
The documentation for this class was generated from the following file native/inc/doppler/carrier_acq/carrier_acq_core.h