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 "clib_common.h"#include "dp_state.h"#include "jm_perf.h"#include "psd/psd_core.h"#include "detector/detector_core.h"#include "detection/detection_core.h"#include "spectral/spectral_core.h"#include "corr/corr_core.h"#include "fft/fft_core.h"#include "acc_trace/acc_trace_core.h"
Classes¶
| Type | Name |
|---|---|
| struct | carrier_acq_state_t CarrierAcquisition state. |
Public Functions¶
| Type | Name |
|---|---|
| carrier_acq_state_t * | 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 | carrier_acq_destroy (carrier_acq_state_t * state) Destroy a carrier_acq instance and release all memory. |
| void | carrier_acq_get_state (const carrier_acq_state_t * state, void * blob) |
| void | carrier_acq_reset (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 | carrier_acq_set_state (carrier_acq_state_t * state, const void * blob) |
| size_t | carrier_acq_state_bytes (const carrier_acq_state_t * state) |
| void | carrier_acq_steps (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:
- 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.
- 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 det_n_noncoh()/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 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 psd_power_twosided()'s own bin order) and the 3-point parabolic sub-bin peak refinement (read directly off 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
carrier_acq_state_t *ca = carrier_acq_create(
4.092e6, 100e3, 0.0, 4, 0, 0.0f, NULL, 0, 1e-3, 0.9, 2.0, true,
100000);
while (!ca->ready && ca->n_blocks < ca->max_n_blocks)
carrier_acq_steps(ca, block, block_len);
double hz = ca->residual_hz; // valid only when ca->ready
carrier_acq_destroy(ca);
Public Functions Documentation¶
function carrier_acq_create¶
Create a carrier_acq instance.
carrier_acq_state_t * 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
)
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.zero_padPSD zero-pad factor (>= 1); see psd_core.h.windowEnum index; 0=hann, 1=kaiser, 2=blackman-harris.betaKaiser beta (ignored for hann/blackman-harris).psd_templateKnown PSD-shape template override, length must equal nfft = next_pow2(round(sample_rate_hz /resolution_hz) * zero_pad); 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 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 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 carrier_acq_destroy¶
Destroy a carrier_acq instance and release all memory.
Parameters:
stateMay be NULL.
function carrier_acq_get_state¶
function 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 carrier_acq_set_state¶
function carrier_acq_state_bytes¶
function 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.
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/carrier_acq/carrier_acq_core.h