Skip to content

Python Acquire API

The doppler.acquire module provides CarrierAcquisition — a PSDMF (power-spectral-density matched-filter) residual-carrier frequency estimator. It runs after doppler.dsss.Acquisition's own coarse Doppler search as a one-shot refinement stage: non-coherently average the incoming stream's power spectrum, then circularly correlate that average against a known power spectrum shape (the default is the average PSD of a random rectangular-pulse BPSK stream, a sinc²; psd_template overrides it for a different pulse shape or modulation) to find the residual carrier offset.

Source: src/doppler/acquire/__init__.py

See the CarrierAcquisition: RRC Pulse Shaping gallery page for a worked example showing why the template matters.


CarrierAcquisition — PSDMF residual-carrier estimation

Composes doppler.spectral.PSD (FFT + window + non-coherent power averaging), doppler.spectral.CorrDetector (FFT-based correlation of the averaged power against the known template, plus a noise-referenced test statistic), and doppler.detection's Pfa/Pd statistics (the same ones Acquisition itself is built on) for the detection gate. sequential (test every block, adaptive) vs. non-sequential (a fixed dwell_target wait) mirror ~/legacy-commz's own FrequencyAcquisition reference — max_n_blocks is sequential mode's own give-up cap, deliberately independent of dwell_target.

CarrierAcquisition

Create a carrier_acq instance.

Parameters:

Name Type Description Default
sample_rate_hz float

Sample rate of the input stream, Hz (required).

required
symbol_rate_hz float

Symbol rate, Hz -- builds the default template (required).

required
resolution_hz float

Desired FFT frequency resolution, Hz. <= 0.0 is a sentinel meaning "auto": symbol_rate_hz/10.0.

0.0
zero_pad int

PSD zero-pad factor (>= 1); see psd_core.h.

4
window Literal['hann', 'kaiser', 'blackman-harris']

Enum index; 0=hann, 1=kaiser, 2=blackman-harris.

"hann"
beta float

Kaiser beta (ignored for hann/blackman-harris).

0.0
psd_template NDArray[float32]

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.

...
pfa float

Target per-test false-alarm probability.

1e-3
pd float

Target detection probability.

0.9
design_snr float

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.

2.0
sequential bool

True: test for a detection after EVERY block (the per-block CFAR ratio threshold -- see _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.

True
max_n_blocks int

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.

100000

Examples:

>>> 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

ready property

ready: bool

True once a detection has fired (or the dwell_target give-up cap was reached) -- residual_hz is only meaningful once this is true.

residual_hz property

residual_hz: float

Sub-bin-refined residual carrier frequency estimate, Hz. Valid only when ready is true.

n_blocks property

n_blocks: int

Number of n_fft-length blocks actually folded into the PSD average so far.

dwell_target property

dwell_target: int

Non-sequential mode's precomputed fixed wait count, from det_n_noncoh(design_snr, ...) at construction. Ignored by sequential mode's own give-up bound -- see max_n_blocks.

max_n_blocks property

max_n_blocks: int

Sequential mode's own give-up cap (independent of dwell_target) -- the max_n_blocks constructor argument, echoed back.

nfft property

nfft: int

PSD transform length (next_pow2(n_fft*zero_pad)) -- the length any caller-supplied template array must match.

steps

steps(x: NDArray[complex64]) -> None

Fold raw complex samples into the running PSD average and test for a detection; any chunk size across repeated calls (a partial trailing block carries to the next call).

Parameters:

Name Type Description Default
x NDArray[complex64]

Raw complex input samples (cf32).

required

Examples:

>>> 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

reset

reset() -> None

Discard the running PSD average and detection state; counters return to zero.

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.

Examples:

>>> 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

state_bytes

state_bytes() -> int

Size in bytes of this object's serialized state.

The exact length get_state returns and set_state requires. It depends on how the object was constructed (state arrays are sized at construction), so read it from the instance rather than assuming a constant.

Raises RuntimeError if the CarrierAcquisition has already been destroyed.

Returns:

Type Description
int

Byte length of one serialized state blob.

get_state

get_state() -> bytes

Serialize this object's mutable state to bytes.

Captures exactly the state that evolves as the object runs, so a blob taken now and restored later resumes from this point. Construction parameters are not included: restore into an object built the same way.

The blob is opaque and always state_bytes() long. Its layout is an implementation detail of the C core and is not a stable format across builds.

Raises RuntimeError if the CarrierAcquisition has already been destroyed.

Returns:

Type Description
bytes

Opaque snapshot, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

Overwrites the live state in place; the object keeps the parameters it was constructed with. Length is validated against state_bytes() before the blob is handed to the C core, and the core may reject it as well.

Raises TypeError if blob is not bytes, ValueError if its length differs from state_bytes() or the core rejects it, and RuntimeError if the CarrierAcquisition has already been destroyed.

Parameters:

Name Type Description Default
blob bytes

A get_state() blob from this type, exactly state_bytes() long.

required

destroy

destroy() -> None

Release the underlying C resources immediately.

Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.

Idempotent: calling it again on an already-released object does nothing. Every other method raises RuntimeError once it has run.

__enter__

__enter__() -> CarrierAcquisition

Enter a context manager, returning this object.

Lets a CarrierAcquisition be used in a with statement so its C resources are released deterministically on exit rather than at collection time.

Returns:

Type Description
CarrierAcquisition

This same object, not a copy.

__exit__

__exit__(
    exc_type: object | None = ...,
    exc: object | None = ...,
    tb: object | None = ...,
) -> None

Exit a context manager, releasing the CarrierAcquisition.

Equivalent to calling destroy(). Returns None, so an exception raised inside the with body propagates normally; this never suppresses one.

Parameters:

Name Type Description Default
exc_type object | None

Exception class, or None. Ignored.

...
exc object | None

Exception instance, or None. Ignored.

...
tb object | None

Traceback object, or None. Ignored.

...

GalleryCarrierAcquisition: RRC Pulse Shaping, Gallery DesignDsssReceiver Specifications