Skip to content

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_hz Sample rate of the input stream, Hz (required).
  • symbol_rate_hz Symbol rate, Hz builds the default template (required).
  • resolution_hz Desired FFT frequency resolution, Hz. <= 0.0 is a sentinel meaning "auto": symbol_rate_hz/10.0.
  • zero_pad PSD zero-pad factor (>= 1); see psd_core.h.
  • window Enum index; 0=hann, 1=kaiser, 2=blackman-harris.
  • beta Kaiser beta (ignored for hann/blackman-harris).
  • psd_template Known 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_len Length of psd_template (0 if not supplied).
  • pfa Target per-test false-alarm probability.
  • pd Target detection probability.
  • design_snr Assumed 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.
  • sequential True: 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_blocks Sequential 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.

void carrier_acq_destroy (
    carrier_acq_state_t * state
) 

Parameters:

  • state May be NULL.

function carrier_acq_get_state

void carrier_acq_get_state (
    const carrier_acq_state_t * state,
    void * blob
) 

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.

void carrier_acq_reset (
    carrier_acq_state_t * state
) 

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:

  • state Must 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

int carrier_acq_set_state (
    carrier_acq_state_t * state,
    const void * blob
) 

function carrier_acq_state_bytes

size_t carrier_acq_state_bytes (
    const carrier_acq_state_t * state
) 

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.

void carrier_acq_steps (
    carrier_acq_state_t * state,
    const float _Complex * x,
    size_t x_len
) 

Parameters:

  • state Must be non-NULL.
  • x Raw complex input samples (cf32).
  • x_len Number of samples in x.
    >>> 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_MAGIC `DP_FOURCC ('C', 'A', 'Q', 'R')`

define CARRIER_ACQ_STATE_VERSION

#define CARRIER_ACQ_STATE_VERSION `1u`


The documentation for this class was generated from the following file native/inc/carrier_acq/carrier_acq_core.h