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
¶
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
¶
Sub-bin-refined residual carrier frequency estimate, Hz. Valid only when ready is true.
n_blocks
property
¶
Number of n_fft-length blocks actually folded into the PSD average so far.
dwell_target
property
¶
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
¶
Sequential mode's own give-up cap (independent of dwell_target) -- the max_n_blocks constructor argument, echoed back.
nfft
property
¶
PSD transform length (next_pow2(n_fft*zero_pad)) -- the length any caller-supplied template array must match.
steps
¶
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
¶
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
¶
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
¶
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, |
set_state
¶
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 |
required |
destroy
¶
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 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. |
...
|
Related pages¶
Gallery — CarrierAcquisition: RRC Pulse Shaping, Gallery Design — DsssReceiver Specifications