Python Resample API¶
Three resampler implementations backed by the native C library — all accept
and return complex64 NumPy arrays with state preserved across calls. For the
polyphase interpolator/decimator architecture behind Resampler and
RateConverter, see the Resampler design notes.
Source:
src/doppler/resample/__init__.py
Which class to use¶
| Class | Algorithm | Rate | Best for |
|---|---|---|---|
RateConverter |
Auto-selected cascade | any | Single-class interface for all rates |
Resampler |
Polyphase (4096 phases × 19 taps) | any | Custom Kaiser spec or execute_ctrl |
HalfbandDecimator |
Halfband 2:1 CF32 | 0.5 (fixed) | First stage in a hand-tuned DDC chain |
HalfbandDecimatorDp |
Halfband 2:1 CF64 | 0.5 (fixed) | Double-precision DDC chain |
HalfbandDecimatorR2C |
Halfband 2:1 F32→CF32 | 0.5 (fixed) | Real ADC input → complex baseband |
CIC |
Cascaded integrator-comb | 1/R (fixed) | High-rate first decimation stage |
RateConverter — automatic cascade¶
Selects the cheapest cascade of CIC, HalfbandDecimator, and/or polyphase
Resampler stages for the requested rate ratio at construction time. The
cascade is rebuilt transparently whenever rate is changed.
Stage selection¶
| Condition (D = 1/rate) | Cascade |
|---|---|
| rate ≥ 1.0 or D < 2 | Resampler(rate) |
| D ≈ 2 | HalfbandDecimator |
| D ≈ 4 | HalfbandDecimator → HalfbandDecimator |
| D = 2ⁿ, n ≥ 3, D ≤ 4096 | CIC(D) |
| D ≥ 8, non-power-of-2 | CIC(R*) → Resampler(R*/D) |
| 2 ≤ D < 8, non-integer | Resampler(rate) |
where R* = nearest power-of-two to D.
from doppler.resample import RateConverter
import numpy as np
x = np.random.randn(4096).astype(np.complex64)
rc = RateConverter(0.5) # HalfbandDecimator
y = rc.execute(x) # len(y) = 2048
rc = RateConverter(0.125) # CIC(8)
y = rc.execute(x) # len(y) = 512
rc = RateConverter(0.1) # CIC(8) → Resampler(0.8)
y = rc.execute(x) # len(y) ≈ 410
print(rc.stages) # ['CIC(8)', 'Resampler(0.8)']
# Change rate — cascade rebuilt, filter state reset
rc.rate = 0.25
print(rc.stages) # ['HalfbandDecimator', 'HalfbandDecimator']
MatchedRateConverter — the cascade IS the matched filter¶
The same object built by a different constructor: the terminal stage carries a
pulse-shaped bank instead of the Kaiser anti-alias one, so one dot product does
the rate conversion and the matched filtering, and that stage's polyphase arm
is the fractional timing delay execute_ctrl steers.
from doppler.resample import MatchedRateConverter
# Two samples per symbol out of a 17.33-samples-per-symbol stream.
mf = MatchedRateConverter(rate=2 / 17.333333333, pulse="rrc", beta=0.35,
span=8, pulse_sps=2.0)
print(mf.stages) # ['CIC(8)', 'Resampler(0.923077,rrc)']
print(mf.bank_shape) # [1024, 40] — arms x taps, set by the OUTPUT rate
y = mf.execute(x)
Three things it does that the plain constructor cannot:
- The terminal fractional stage always exists. The ordinary planner drops
it for an exact power-of-two decimation, so
rate = 2/64plans a bareCIC(32)with nothing steerable at the end; here it is appended (at rate 1.0 if there is no rate left to correct), because that stage is simultaneously the matched filter and the timing element. - The bank is sized by the POST-decimation rate — the same ~34 taps per arm at 4 input samples per symbol and at 256, where matched-filtering at the input rate would need 4225.
- CIC droop folds into the bank, exactly, at a handful of taps per arm and
no extra stage — worth 28 dB of EVM on a CIC plan, which is why
compensatedefaults to 1 here and 0 on the plain converter.
bank_shape is [] when the cascade ends in an integer decimator and so has
no bank to describe; narrow_pulse (and a UserWarning at construction) flags
the one degenerate configuration — pulse="iandd" with fewer than four output
samples per symbol, where the one-symbol-wide rectangle's matched filter is a
2–3 tap sum.
Streaming¶
State is preserved across execute() calls, so splitting a stream at any block
boundary is byte-identical to one large call. execute() returns a zero-copy
view into an internal buffer — process() it (or .copy() it) before the
next execute():
rc = RateConverter(0.5)
iq_stream = np.array_split(x, 4) # CF32 blocks, any length
process = np.abs # a real downstream consumer
for block in iq_stream: # CF32 blocks, any length
y = rc.execute(block)
process(y) # consume now; the next execute() reuses y's buffer
View lifetime
The returned array is valid only until you next touch the converter.
Copy it to retain it. The next execute() reuses the buffer in place;
reset(), assigning .rate, or a block larger than any seen so far
reallocates it (a previously-returned array then dangles). The fixed-block
consume-then-next loop above needs no copy. This is the convention for every
variable_output execute in doppler (Resampler, FIR, the DDC chain, …).
Serializable state — elastic resume¶
A RateConverter can hand its entire running state to a fresh instance and
resume bit-for-bit — the basis for checkpointing, migrating a stream between
processes, or scaling a pipeline across pods. get_state() returns a bytes
blob; set_state() restores it into an identically-built converter:
>>> import numpy as np
>>> from doppler.resample import RateConverter
>>> rng = np.random.default_rng(0)
>>> x = (rng.standard_normal(6000)
... + 1j * rng.standard_normal(6000)).astype(np.complex64)
>>> # Worker A processes the first half, checkpoints its state, then exits.
>>> a = RateConverter(0.5)
>>> head = np.array(a.execute(x[:2600]))
>>> blob = a.get_state() # bytes — persist or ship to another worker
>>> len(blob) == a.state_bytes()
True
>>> del a
>>> # Worker B restores the exact mid-stream state and resumes.
>>> b = RateConverter(0.5) # same rate ⇒ same cascade
>>> b.set_state(blob)
>>> tail = np.array(b.execute(x[2600:]))
>>> # The hand-off is seamless: identical to one uninterrupted run.
>>> ref = RateConverter(0.5)
>>> reference = np.array(ref.execute(x))
>>> np.array_equal(np.concatenate([head, tail]), reference)
True
The blob carries a self-describing envelope, so a truncated, corrupted, or
wrong-configuration blob is rejected (ValueError) rather than silently
reinterpreted — set_state either restores exactly or leaves the converter
untouched:
>>> for bad in [blob[:-1], # truncated
... RateConverter(0.25).get_state()]: # different rate ⇒ different size
... try:
... RateConverter(0.5).set_state(bad)
... except ValueError:
... print("rejected")
rejected
rejected
get_state/set_state/state_bytes are uniform across every serializable
doppler type — see the State Serialization design.
Functional wrapper¶
rate_convert() creates a RateConverter on the first call and returns it
so it can be passed back to maintain state:
from doppler.resample import rate_convert
y1, rc = rate_convert(x, 0.5)
y2, rc = rate_convert(x, 0.5, rc=rc) # same converter, state preserved
CIC droop compensation¶
Pass compensate=1 to append a passband-droop compensating FIR after any
CIC stage. The FIR is designed with ciccompmf(N=4, R=R, M=7):
rc = RateConverter(0.125, compensate=1)
# cascade: CIC(8) → FIR-comp(7 taps)
print(rc.stages) # ['CIC(8)+FIR']
Resampler — general polyphase¶
Built-in Kaiser bank (60 dB rejection, 0.4/0.6 pass/stop). Works for any rate — integer, fractional, and irrational.
from doppler.resample import Resampler
import numpy as np
x = np.random.randn(4096).astype(np.complex64)
# Decimate 2×
r = Resampler(0.5)
y = r.execute(x) # len(y) ≈ 2048
# Interpolate 3×
r2 = Resampler(3.0)
y2 = r2.execute(x) # len(y2) ≈ 12288
# Fractional — irrational rate is fine
r3 = Resampler(44100 / 48000)
y3 = r3.execute(x)
Rate-controlled resampling (FM/Doppler correction)¶
Per-sample rate deviation via execute_ctrl:
doppler_correction = np.linspace(-1.0, 1.0, 4096) # per-sample deviation
ctrl = np.zeros(4096, dtype=np.complex64) # deviation in norm_freq units
ctrl.real = 1e-4 * doppler_correction
y = r.execute_ctrl(x, ctrl)
HalfbandDecimator — fixed 2:1 decimation¶
Symmetric FIR halfband filter; every other output sample is the identity (zero multiply) which halves the compute cost vs. a general FIR. Use as the first stage in a multi-stage DDC chain.
from doppler.resample import HalfbandDecimator, kaiser_beta, kaiser_num_taps
import numpy as np
# Design a Kaiser halfband prototype (the caller supplies the FIR taps).
ntaps = kaiser_num_taps(2, 60.0, 0.4, 0.6) | 1 # odd length (19 taps)
n = np.arange(ntaps) - (ntaps - 1) // 2
h = np.sinc(n / 2.0) * np.kaiser(ntaps, kaiser_beta(60.0))
h = (h / h.sum()).astype(np.float32) # unit DC gain
decim = HalfbandDecimator(h=h) # caller-supplied Kaiser prototype
x = np.random.randn(4096).astype(np.complex64)
y = decim.execute(x) # len(y) = 2048
Phase-continuous across blocks:
next_stage = np.abs # a real downstream consumer
for block in iq_stream: # CF32 arrays, any length
y = decim.execute(block)
next_stage(y)
HalfbandDecimatorQ15 — fixed-point (Q15) variant¶
HalfbandDecimatorQ15 is a fixed-point halfband 2:1 decimator for
interleaved-I/Q int16 streams — the integer-pipeline counterpart to
HalfbandDecimator. The FIR branch taps are supplied as float and converted
internally to Q15 (with the ×0.5 polyphase rate scaling). The halfband
prototype is sparse — every other tap is zero — so you supply only the
non-zero branch taps, not the full prototype. See the
HalfbandDecimatorQ15 example for the
passband/stopband response.
import numpy as np
from doppler.resample import HalfbandDecimatorQ15
# non-zero branch taps of a halfband prototype (float; converted to Q15)
taps = np.array([-0.03, 0.28, 0.5, 0.28, -0.03], np.float32)
dec = HalfbandDecimatorQ15(taps)
x = (np.random.randn(4096) * 8192).astype(np.int16) # interleaved I/Q
y = dec.execute(x) # int16, half the length
CIC — cascaded integrator-comb decimator¶
Fixed-rate integer decimation by a power-of-two factor R. Fixed at N=4
stages, M=1. Runs directly on the input stream at full rate — no
multipliers, just integrators and combs. Pair with ciccompmf to correct
passband droop.
from doppler.resample import CIC, ciccompmf
import numpy as np
cic = CIC(16) # R=16, N=4 (fixed), M=1 (fixed)
x = np.random.randn(4096).astype(np.complex64)
y = cic.decimate(x) # len(y) = 256
# Design a 7-tap droop compensator (runs at output rate)
h = ciccompmf(N=4, R=16, M=7) # NDArray[float64], length 7
ciccompmf — CIC droop-compensator design¶
Closed-form maximally-flat FIR compensator (Molnar & Vucic, IEEE TCAS-II 58(12):926–930, 2011). Returns a symmetric FIR kernel that corrects CIC passband droop; apply it at the decimated output rate.
from doppler.resample import ciccompmf
h = ciccompmf(N=4, R=16, M=7)
# h is NDArray[float64] of length M=7, DC gain ≈ 1.0
Valid M: odd 1–19, even 2–18. Out-of-range M returns all-zeros.
Farrow — fractional-delay interpolator¶
Farrow is a lightweight selectable-order fractional-delay interpolator
(linear / parabolic / cubic) — the lean alternative to a full polyphase
resampler when all you need is a fractional tap, e.g. the interpolator inside a
symbol-timing loop. All three orders share one 4-tap delay line and a fixed
2-sample group delay (so a driving loop is order-agnostic) and are symmetric
about the interpolation point (linear-phase → no delay bias). The fractional
offset µ ∈ [0,1) is meant to come from an integer timing NCO, so timing stays
exact while only the interpolation is floating point.
See the Farrow gallery page for the response of each order.
from doppler.resample import Farrow
f = Farrow(order="cubic") # "linear" | "parabolic" | "cubic"
y = f.delay(x, mu=0.3) # constant fractional delay (mu) of a cf32 block
Kaiser filter-design helpers¶
Two closed-form helpers expose the Kaiser window design the polyphase
Resampler uses internally, for callers rolling their own FIR spec.
kaiser_beta(atten) maps a stopband attenuation (dB) to the Kaiser beta
shape parameter (the FIR-stopband formula 0.1102·(A−8.7), distinct from the
window-sidelobe formula in spectral); kaiser_num_taps(num_phases, atten, pb, sb) returns the tap count meeting an attenuation over a pass/stop band edge.
from doppler.resample import kaiser_beta, kaiser_num_taps
beta = kaiser_beta(60.0) # 60 dB stopband → beta ≈ 5.65
ntaps = kaiser_num_taps(4096, 60.0, 0.4, 0.6) # taps for a 0.4/0.6 transition
resample
¶
Sample-rate conversion: polyphase resampling (Resampler, RateConverter), halfband decimation (HalfbandDecimator), CIC decimation, and Farrow fractional resampling.
Examples:
>>> import numpy as np
>>> from doppler.resample import Resampler
>>> Resampler(rate=2.0).execute(np.ones(100, np.complex64)).size
200
HalfbandDecimator
¶
Create a HalfbandDecimator with caller-supplied FIR taps. Implements a
2:1 polyphase halfband decimator over CF32 IQ. The caller provides the FIR
branch coefficient array h; use doppler.resample.kaiser_num_taps(2,
atten, pb, sb) to size it and scipy or the built-in bank helper to design
the prototype. Output length is approximately x_len / 2 per execute() call.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
h
|
NDArray[float32]
|
Float32 FIR branch coefficients, length num_taps. Must be a symmetric halfband prototype (antisymmetric even-indexed taps zeroed). |
required |
Examples:
>>> from doppler.resample import HalfbandDecimator
>>> import numpy as np
>>> h = np.array([0.0625, 0.25, 0.375, 0.25, 0.0625],
... dtype=np.float32)
>>> hb = HalfbandDecimator(h=h)
>>> hb.num_taps, hb.rate
(5, 0.5)
rate
property
¶
Fixed decimation rate — always 0.5. The halfband decimator is structurally 2:1; this property exists for API parity with Resampler and RateConverter.
num_taps
property
¶
Number of FIR branch taps as passed to create. The all-pass (even-phase) branch has no taps; only the odd-phase FIR branch has length num_taps. The total prototype length is 2 * num_taps - 1.
execute
¶
Decimate x by 2 using the polyphase halfband FIR filter. Processes every second input sample through the FIR branch and passes the other branch through the all-pass (zero-delay) path. State persists between calls — contiguous blocks give identical output to one large block. Output length is floor(x_len / 2).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
CF32 input array. Length must be even for exact half-rate output; odd lengths write floor(x_len/2). |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
CF32 decimated output; length is min(floor(x_len / 2), max_out). |
Examples:
execute_max_out
¶
Always returns HBDECIM_MAX_OUT.
Returns:
| Type | Description |
|---|---|
int
|
Output. |
reset
¶
Zero all delay lines. Coefficients and num_taps preserved. Call between signal bursts to suppress transient ringing from prior filter state. The next execute() after reset produces the same output as a freshly created decimator fed the same input.
Examples:
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 HalfbandDecimator 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 HalfbandDecimator 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 HalfbandDecimator 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 HalfbandDecimator be used in a with statement so its C
resources are released deterministically on exit rather than at
collection time.
Returns:
| Type | Description |
|---|---|
HalfbandDecimator
|
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 HalfbandDecimator.
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. |
...
|
Resampler
¶
Create a Resampler with the built-in 4096×19 Kaiser bank. The bank provides ~60 dB alias rejection with 0.4/0.6 pass/stop normalised cutoffs. Pass rate >= 1.0 to interpolate (upsample); pass rate < 1.0 to decimate (downsample). For a custom bank use Resampler_create_custom() instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rate
|
float
|
Output-to-input sample rate ratio (any positive float). Values >= 1.0 interpolate; values < 1.0 decimate. |
0.0
|
Examples:
>>> from doppler.resample import Resampler
>>> import numpy as np
>>> r = Resampler(rate=2.0)
>>> r.num_phases, r.num_taps
(4096, 19)
>>> r.rate
2.0
rate
property
writable
¶
Get / set the output-to-input sample rate ratio. The setter recomputes the phase increment immediately; the delay line and phase accumulator are preserved so in-stream rate changes are glitch-free. Switching sign of (rate - 1) (i.e. crossing the boundary between interp and decim modes) requires a fresh create().
num_phases
property
¶
Number of polyphase branches in the filter bank. Always a power of two. The built-in bank has 4096 phases giving sub-sample timing resolution of 1/4096 of an input sample period.
num_taps
property
¶
Taps per polyphase branch. Total prototype filter length is num_phases * num_taps - 1. The built-in bank uses 19 taps per branch.
execute
¶
Resample a block of CF32 samples at the fixed base rate. Uses the dual-mode polyphase engine: output-driven for rate >= 1 (interpolation), input-driven transposed-form for rate < 1 (decimation). State carries over between calls, so contiguous blocks produce the same result as one large block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
CF32 input samples. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
CF32 output array; length is approximately x_len * rate, capped at max_out. |
Examples:
execute_max_out
¶
Always returns RESAMPLER_MAX_OUT.
Returns:
| Type | Description |
|---|---|
int
|
Output. |
execute_ctrl
¶
Resample with per-sample additive rate deviations. Effective rate
for sample i is base_rate + real(ctrl[i]). Uses a unified
double-precision accumulator that handles both interpolation and
decimation in a single code path — suitable for Doppler-shift
simulation and fractional-sample timing correction. ctrl and x must
have the same length.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
CF32 input samples. |
required |
ctrl
|
NDArray[complex64]
|
CF32 array, same length as x; only the real part is used as a per-sample rate addend. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
CF32 output array; length depends on accumulated rate deviations, capped at max_out. |
Examples:
reset
¶
Zero the delay line and phase accumulator. Rate and polyphase bank are preserved so the resampler can be resumed at the same ratio. Zeroing state eliminates transient artefacts when starting a new signal burst.
Examples:
execute_ctrl_max_out
¶
Max samples execute_ctrl() can emit, for any input block.
The bound is the resampler's fixed internal capacity
(RESAMPLER_MAX_OUT), not a function of the block about to be
passed — which is why this accessor takes no argument.
Returns:
| Type | Description |
|---|---|
int
|
Capacity, in samples, to allocate for an |
Examples:
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 Resampler 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 Resampler 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 Resampler 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 Resampler be used in a with statement so its C resources are
released deterministically on exit rather than at collection time.
Returns:
| Type | Description |
|---|---|
Resampler
|
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 Resampler.
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. |
...
|
CIC
¶
Create a 4-stage, M=1 CIC decimation filter. Allocates the state struct on the heap and pre-computes the normalisation right-shift (CIC_N * log2(R) bits). All integrator and comb accumulators are zeroed; the first output arrives after R input samples. Returns NULL for invalid R or OOM. Input amplitude is bounded: |Re| and |Im| <= 1.0. A component beyond +-1.0 is clipped at the boundary before any filtering; the sample stream gives no sign of it, so check the sticky clipped flag. Unlike doppler's floating-point blocks this one is not scale-free -- scale the input into range first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
R
|
int
|
Decimation ratio. Must be a power of two in |
16
|
Examples:
clipped
property
¶
True if any input component has exceeded the +-1.0 bound since the last reset(). Sticky, and free to read: the CIC's boundary comparisons run on every sample anyway, so it records something the sample stream cannot tell you -- a clipped stream still looks entirely plausible (finite, no NaN, merely distorted), so this flag is the only reliable check.
reset
¶
Zero all integrator and comb accumulators; preserve R and shift. The first output sample after reset arrives after R more input samples, matching post-create behaviour. Use between signal bursts to eliminate transient artefacts caused by residual pipeline state.
Examples:
reconfigure
¶
Change the decimation ratio in place and reset all filter state.
Recomputes the normalisation shift (CIC_N * log2(R)) and zeros all
accumulators so the filter behaves exactly like a freshly created one
with the new R. Silently ignores R values that are not a power-of-two
in [2, 4096] — the state is left unchanged in that case.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
R
|
int
|
New decimation ratio. Same constraints as cic_create(). |
required |
Examples:
decimate
¶
Decimate a block of CF32 samples through the CIC pipeline. Each sample is converted to offset-binary UQ16, pushed through CIC_N integrators (unsigned wrapping), and when the phase counter reaches R the integrated value is passed through CIC_N M=1 comb stages and converted back to CF32. State persists between calls. Feeding blocks that are multiples of R gives predictable output counts (exactly n_in/R samples per block).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
CF32 output array; length is min(floor((phase + n_in) / R), max_out). |
Notes
Input amplitude is bounded: |Re| and |Im| <= 1.0. A component beyond +-1.0 is clipped at the boundary before filtering; the sample stream gives no sign of it, so check the sticky clipped flag. Scale the input into range first; see the file header.
Examples:
decimate_max_out
¶
Upper bound on decimate output — returns 0 (lazy-alloc signal).
The Python extension allocates n_in elements on the first call.
Since n_in >= ceil(n_in/R) = n_out for all R >= 1, the buffer is
always large enough as long as block size stays consistent.
Returns:
| Type | Description |
|---|---|
int
|
Output. |
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 CIC 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 CIC 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 CIC 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 CIC be used in a with statement so its C resources are
released deterministically on exit rather than at collection time.
Returns:
| Type | Description |
|---|---|
CIC
|
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 CIC.
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. |
...
|
RateConverter
¶
Create a rate converter for the given output/input rate ratio. Selects the cheapest cascade of CIC, HalfbandDecimator, and/or polyphase Resampler stages at construction time (see file header for the selection table). Setting compensate=1 appends a closed-form Molnar-Vucic CIC droop-compensating FIR after any CIC stage, which improves passband flatness at the cost of one extra FIR stage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rate
|
float
|
Output-to-input sample rate ratio. Any positive float. |
1.0
|
compensate
|
int
|
Non-zero to append a CIC passband-droop compensating FIR after any CIC stage. |
0
|
Examples:
>>> from doppler.resample import RateConverter
>>> rc = RateConverter(rate=0.5, compensate=0)
>>> rc.rate
0.5
rate
property
writable
¶
Get / set the output-to-input sample rate ratio. The setter rebuilds the entire cascade (new stage selection, new sub-objects) and resets all filter memories — equivalent to destroying and recreating with the new rate. Setting rate <= 0 is silently ignored.
clipped
property
¶
True if any planned CIC stage has clipped its input since the last
reset(). The cascade inherits the CIC's input bound (|Re|, |Im| <=
1.0) whenever stages names a CIC -- any decimation by 8 or more. The
clip is invisible in the samples (finite, no NaN, merely distorted), so
this is the only reliable check, and it is free: the boundary
comparisons run on every sample regardless. Always False for a cascade
with no CIC stage -- those plans are scale-free.
narrow_pulse
property
¶
True when a rectangular pulse was selected with fewer than four
output samples per symbol, where its matched filter degenerates to a
2-3 tap sum. Construction also raises a UserWarning; this is the same
diagnostic to pull rather than catch. Always False for pulse="rrc"
and for a plain converter.
stages
property
¶
Stage labels for the planned cascade, e.g. ['CIC(8)',
'Resampler(0.8)']. A terminal stage carrying a pulse-shaped bank names
its pulse: 'Resampler(0.923077,rrc)'.
bank_shape
property
¶
[num_phases, num_taps] of the terminal polyphase stage, or []
when the cascade ends in an integer decimator and so has no bank to
describe. num_taps is the per-output MAC count and, times
num_phases, the bank's size in floats. With a pulse selected it is
set by the terminal stage's rate rather than the input rate -- which is
what keeps a matched filter affordable at a high input
samples-per-symbol: the same 34 taps per arm at 4 samples/symbol and at
256, where filtering at the input rate would need 4225.
execute
¶
Convert a block of CF32 samples through the cascade. Passes input through each stage in order, ping-ponging between two intermediate buffers. State persists between calls, so contiguous calls on sequential blocks give the same result as one large call. Output length is approximately n_in * rate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
CF32 output array; length is approximately n_in * rate. |
Examples:
execute_max_out
¶
Upper bound on execute output for a standard 65536-sample block.
Returns (size_t)(65536 * max(rate, 1.0)) + 2. The Python extension uses
this to pre-allocate the output buffer on the first execute call.
Returns:
| Type | Description |
|---|---|
int
|
Output. |
execute_ctrl
¶
Convert a block, steering the cascade's fractional stage by ctrl.
The control-port form of RateConverter_execute(): the fixed integer
stages (HalfbandDecimator / CIC) run unchanged, and the scalar rate
deviation ctrl is forwarded to the terminal polyphase Resampler
stage's accumulator (via resamp_execute_ctrl_push) — so its effective
rate becomes stage_rate + ctrl for this call. This exposes the
fractional tail's control port that RateConverter_execute() hides: a
timing/rate-tracking loop can decimate a high input rate cheaply
through the HB/CIC stages and then arbitrary-rate + strobe-align in the
last stage, updating ctrl per block.
ctrl is referenced to the terminal stage's (post-decimation) rate,
not the overall rate. It is meaningful only when the cascade actually
ends in a Resampler stage; a pure integer HB/CIC cascade has no
fractional stage to steer, so this falls through to
RateConverter_execute() (ctrl ignored).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
CF32 input block. |
required |
ctrl
|
float
|
Rate deviation added to the terminal Resampler stage's rate. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
CF32 output array; length tracks the accumulated effective rate. |
Examples:
>>> from doppler.resample import RateConverter
>>> import numpy as np
>>> rc = RateConverter(rate=0.8, compensate=0) # -> Resampler(0.8)
>>> x = np.ones(1000, dtype=np.complex64)
>>> rc.execute_ctrl(x, 0.0).shape[0] # base rate: 1000 -> 800
800
>>> rc2 = RateConverter(rate=0.8, compensate=0)
>>> rc2.execute_ctrl(x, 0.05).shape[0] # +ctrl speeds the tail up
850
execute_ctrl_push
¶
Push ONE input sample; emit whatever outputs it completes.
The per-input streaming form of RateConverter_execute_ctrl(), and the
only form a closed loop can use: a block call must know its whole
ctrl history up front, whereas a timing loop computes each correction
from the outputs already emitted. Feeding a stream one sample at a
time through this reproduces RateConverter_execute_ctrl() on the same
block bit-for-bit when ctrl is held constant (the cascade is
block-boundary invariant), so the cheap block form stays correct for
open-loop use.
The integer HB/CIC stages consume the sample and emit at most one intermediate sample each; the terminal Resampler stage then emits 0 outputs (a decimator between strobes — the common case), 1, or several (an interpolator). A cascade with no terminal Resampler ignores ctrl.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
complex
|
One CF32 input sample. |
required |
ctrl
|
float
|
Rate deviation added to the terminal stage's rate for this input (referenced to the terminal, post-decimation rate). |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
CF32 array of the outputs completed by this input (0, 1, or more). |
Examples:
>>> from doppler.resample import RateConverter
>>> import numpy as np
>>> rc = RateConverter(rate=0.8, compensate=0) # -> Resampler(0.8)
>>> x = (np.arange(10, dtype=np.float32) + 1).astype(np.complex64)
>>> # a decimator emits 0 between strobes, 1 on a strobe:
>>> [rc.execute_ctrl_push(complex(v), 0.0).shape[0] for v in x]
[0, 1, 1, 1, 1, 0, 1, 1, 1, 1]
reset
¶
Zero all sub-stage filter memories. Rate, stage count, and stage types are preserved. Processing from a reset state produces the same output as a freshly created converter fed the same input. Use between signal bursts to suppress transient artefacts from prior filter memory.
Examples:
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 RateConverter 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 RateConverter 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 RateConverter 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 RateConverter be used in a with statement so its C resources
are released deterministically on exit rather than at collection time.
Returns:
| Type | Description |
|---|---|
RateConverter
|
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 RateConverter.
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. |
...
|
Farrow
¶
Create a Farrow interpolator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
order
|
Literal['linear', 'parabolic', 'cubic']
|
0 = linear, 1 = parabolic, 2 = cubic. |
"cubic"
|
Examples:
Create with defaults:
delay
¶
Apply a constant fractional delay of mu samples to a cf32 block
via the Farrow interpolator; output[i] is the input interpolated at i -
group_delay + mu. The first group_delay samples are filling-transient.
Pushes each input sample through the delay line and evaluates the
interpolator at the same fixed offset, so the whole block is delayed by
a constant, non-integer amount. Output sample i is the input
interpolated at i - group_delay + mu, i.e. the stream shifted later
by group_delay - mu samples; the first group_delay outputs are the
delay-line filling transient and should be discarded. Because the
offset is held constant this is the open-loop use of the interpolator —
a timing loop instead steers mu per sample via
farrow_push()/farrow_eval().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
CF32 input samples. |
required |
mu
|
float
|
Fractional delay in samples; the offset in |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
CF32 output array, same length as x, each sample delayed by
|
Examples:
>>> from doppler.resample import Farrow
>>> import numpy as np
>>> f = Farrow(order="cubic")
>>> x = np.arange(8, dtype=np.complex64) # a ramp: exact interp
>>> y = f.delay(x, 0.5) # delay group_delay - 0.5
>>> [round(float(v.real), 4) for v in y] # first 2 are transient
[0.0, -0.0625, 0.4375, 1.5, 2.5, 3.5, 4.5, 5.5]
reset
¶
Clear the interpolator delay line.
Zeroes the 4-tap delay line so the next block starts from a filling transient again, exactly as a freshly created interpolator would. The order (linear / parabolic / cubic) is preserved, so the same object can be reused across independent bursts without rebuilding the polynomial. Call it between unrelated signal segments to stop the tail of one leaking into the head of the next.
Examples:
>>> from doppler.resample import Farrow
>>> import numpy as np
>>> f = Farrow(order="cubic")
>>> _ = f.delay(np.ones(8, dtype=np.complex64), 0.25) # leaves state
>>> f.reset() # back to pristine
>>> x = np.arange(8, dtype=np.complex64)
>>> f.delay(x, 0.5)[3:].real.tolist() # steady part: ramp - 1.5
[1.5, 2.5, 3.5, 4.5, 5.5]
delay_max_out
¶
Extra capacity delay() needs beyond the input length: none.
An out= buffer must hold max(delay_max_out(), len(x))
elements. delay() emits at most one sample per input sample,
so it imposes no requirement of its own and this returns 0 —
len(x) alone is the exact bound. A non-zero value here means
the opposite: that a method can emit more than it is given
(Resampler.execute_ctrl_max_out interpolates, FFT pads to
n), which is the only case the accessor exists to cover.
Returns:
| Type | Description |
|---|---|
int
|
0 — size an |
Examples:
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 Farrow 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 Farrow 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 Farrow 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 Farrow be used in a with statement so its C resources are
released deterministically on exit rather than at collection time.
Returns:
| Type | Description |
|---|---|
Farrow
|
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 Farrow.
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. |
...
|
HalfbandDecimatorQ15
¶
Allocate and initialise a fixed-point halfband 2:1 decimator. The FIR branch coefficients are supplied as float and converted internally to Q15 with a x0.5 polyphase rate scaling. The full halfband prototype is sparse (every other tap is zero); supply only the non-zero FIR branch taps, not the full sparse prototype.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
h
|
NDArray[float32]
|
Float FIR branch coefficients of length num_taps. Must be symmetric
( |
required |
Examples:
>>> import numpy as np
>>> from doppler.resample import HalfbandDecimatorQ15
>>> h = np.array([0.25, 0.5, 0.25], dtype=np.float32)
>>> dec = HalfbandDecimatorQ15(h)
>>> dec.num_taps
3
>>> dec.rate
0.5
num_taps
property
¶
FIR branch length as supplied to the constructor. This is the count of non-zero symmetric taps in the FIR branch, not the full sparse halfband prototype length. Useful for introspection when chaining multiple stages with programmatically computed filter banks.
rate
property
¶
The sample-rate reduction factor; always 0.5 for 2:1 decimation. Exposed as a read-only property so pipelines can query the rate of each stage programmatically without hard-coding the 2:1 assumption.
execute
¶
Decimate a block of interleaved IQ int16 samples by 2. Input must be interleaved int16_t IQ pairs (I₀ Q₀ I₁ Q₁ …); pass a 1-D array of 2*n_complex elements. Each pair of complex input samples produces one complex output sample, so an array of length 2N yields at most N output pairs (2N int16 output values). If n_in is odd the trailing IQ pair is buffered and consumed on the next call.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[int16]
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[int16]
|
min(available, max_out) COMPLEX samples -- twice that many int16_t values. |
Examples:
>>> import numpy as np
>>> from doppler.resample import HalfbandDecimatorQ15
>>> h = np.array([0.25, 0.5, 0.25], dtype=np.float32)
>>> dec = HalfbandDecimatorQ15(h)
>>> x = np.array([1000, 0, 1000, 0, 1000, 0, 1000, 0], dtype=np.int16)
>>> y = dec.execute(x)
>>> y.dtype
dtype('int16')
>>> y.shape
(4,)
>>> y.tolist()
[0, 0, 625, 0]
execute_max_out
¶
Maximum output samples for a given input length.
Returns 0 to trigger the lazy-alloc path in the Python glue: the
output buffer is sized to n_in on first call (always sufficient for 2:1).
Returns:
| Type | Description |
|---|---|
int
|
Output. |
reset
¶
Zero all delay rings and clear the pending-sample flag. After a reset the decimator behaves identically to a freshly constructed instance: the four dual-write delay rings are zeroed and has_pending is cleared, so no partial IQ pair carries over. Call this between unrelated signal segments to prevent inter-segment leakage.
Examples:
>>> import numpy as np
>>> from doppler.resample import HalfbandDecimatorQ15
>>> h = np.array([0.25, 0.5, 0.25], dtype=np.float32)
>>> dec = HalfbandDecimatorQ15(h)
>>> x = np.array([1000, 0, 1000, 0, 1000, 0, 1000, 0], dtype=np.int16)
>>> _ = dec.execute(x)
>>> dec.reset()
>>> y = dec.execute(x)
>>> y.tolist()
[0, 0, 625, 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 HalfbandDecimatorQ15 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 HalfbandDecimatorQ15 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 HalfbandDecimatorQ15 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 HalfbandDecimatorQ15 be used in a with statement so its C
resources are released deterministically on exit rather than at
collection time.
Returns:
| Type | Description |
|---|---|
HalfbandDecimatorQ15
|
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 HalfbandDecimatorQ15.
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. |
...
|
MatchedRateConverter
¶
Create a rate converter for the given output/input rate ratio. Selects the cheapest cascade of CIC, HalfbandDecimator, and/or polyphase Resampler stages at construction time (see file header for the selection table). Setting compensate=1 appends a closed-form Molnar-Vucic CIC droop-compensating FIR after any CIC stage, which improves passband flatness at the cost of one extra FIR stage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rate
|
float
|
Output-to-input sample rate ratio. Any positive float. |
1.0
|
compensate
|
int
|
Non-zero to append a CIC passband-droop compensating FIR after any CIC stage. |
1
|
pulse
|
Literal['iandd', 'rrc']
|
pulse constructor parameter. |
"rrc"
|
beta
|
float
|
beta constructor parameter. |
0.35
|
span
|
int
|
span constructor parameter. |
8
|
pulse_sps
|
float
|
pulse_sps constructor parameter. |
2.0
|
num_phases
|
int
|
num_phases constructor parameter. |
1024
|
Examples:
>>> from doppler.resample import RateConverter
>>> rc = RateConverter(rate=0.5, compensate=0)
>>> rc.rate
0.5
rate
property
writable
¶
Get / set the output-to-input sample rate ratio. The setter rebuilds the entire cascade (new stage selection, new sub-objects) and resets all filter memories — equivalent to destroying and recreating with the new rate. Setting rate <= 0 is silently ignored.
clipped
property
¶
True if any planned CIC stage has clipped its input since the last
reset(). The cascade inherits the CIC's input bound (|Re|, |Im| <=
1.0) whenever stages names a CIC -- any decimation by 8 or more. The
clip is invisible in the samples (finite, no NaN, merely distorted), so
this is the only reliable check, and it is free: the boundary
comparisons run on every sample regardless. Always False for a cascade
with no CIC stage -- those plans are scale-free.
narrow_pulse
property
¶
True when a rectangular pulse was selected with fewer than four
output samples per symbol, where its matched filter degenerates to a
2-3 tap sum. Construction also raises a UserWarning; this is the same
diagnostic to pull rather than catch. Always False for pulse="rrc"
and for a plain converter.
stages
property
¶
Stage labels for the planned cascade, e.g. ['CIC(8)',
'Resampler(0.8)']. A terminal stage carrying a pulse-shaped bank names
its pulse: 'Resampler(0.923077,rrc)'.
bank_shape
property
¶
[num_phases, num_taps] of the terminal polyphase stage, or []
when the cascade ends in an integer decimator and so has no bank to
describe. num_taps is the per-output MAC count and, times
num_phases, the bank's size in floats. With a pulse selected it is
set by the terminal stage's rate rather than the input rate -- which is
what keeps a matched filter affordable at a high input
samples-per-symbol: the same 34 taps per arm at 4 samples/symbol and at
256, where filtering at the input rate would need 4225.
execute
¶
Convert a block of CF32 samples through the cascade. Passes input through each stage in order, ping-ponging between two intermediate buffers. State persists between calls, so contiguous calls on sequential blocks give the same result as one large call. Output length is approximately n_in * rate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
CF32 output array; length is approximately n_in * rate. |
Examples:
execute_max_out
¶
Upper bound on execute output for a standard 65536-sample block.
Returns (size_t)(65536 * max(rate, 1.0)) + 2. The Python extension uses
this to pre-allocate the output buffer on the first execute call.
Returns:
| Type | Description |
|---|---|
int
|
Output. |
execute_ctrl
¶
Convert a block, steering the cascade's fractional stage by ctrl.
The control-port form of RateConverter_execute(): the fixed integer
stages (HalfbandDecimator / CIC) run unchanged, and the scalar rate
deviation ctrl is forwarded to the terminal polyphase Resampler
stage's accumulator (via resamp_execute_ctrl_push) — so its effective
rate becomes stage_rate + ctrl for this call. This exposes the
fractional tail's control port that RateConverter_execute() hides: a
timing/rate-tracking loop can decimate a high input rate cheaply
through the HB/CIC stages and then arbitrary-rate + strobe-align in the
last stage, updating ctrl per block.
ctrl is referenced to the terminal stage's (post-decimation) rate,
not the overall rate. It is meaningful only when the cascade actually
ends in a Resampler stage; a pure integer HB/CIC cascade has no
fractional stage to steer, so this falls through to
RateConverter_execute() (ctrl ignored).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
CF32 input block. |
required |
ctrl
|
float
|
Rate deviation added to the terminal Resampler stage's rate. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
CF32 output array; length tracks the accumulated effective rate. |
Examples:
>>> from doppler.resample import RateConverter
>>> import numpy as np
>>> rc = RateConverter(rate=0.8, compensate=0) # -> Resampler(0.8)
>>> x = np.ones(1000, dtype=np.complex64)
>>> rc.execute_ctrl(x, 0.0).shape[0] # base rate: 1000 -> 800
800
>>> rc2 = RateConverter(rate=0.8, compensate=0)
>>> rc2.execute_ctrl(x, 0.05).shape[0] # +ctrl speeds the tail up
850
execute_ctrl_push
¶
Push ONE input sample; emit whatever outputs it completes.
The per-input streaming form of RateConverter_execute_ctrl(), and the
only form a closed loop can use: a block call must know its whole
ctrl history up front, whereas a timing loop computes each correction
from the outputs already emitted. Feeding a stream one sample at a
time through this reproduces RateConverter_execute_ctrl() on the same
block bit-for-bit when ctrl is held constant (the cascade is
block-boundary invariant), so the cheap block form stays correct for
open-loop use.
The integer HB/CIC stages consume the sample and emit at most one intermediate sample each; the terminal Resampler stage then emits 0 outputs (a decimator between strobes — the common case), 1, or several (an interpolator). A cascade with no terminal Resampler ignores ctrl.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
complex
|
One CF32 input sample. |
required |
ctrl
|
float
|
Rate deviation added to the terminal stage's rate for this input (referenced to the terminal, post-decimation rate). |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
CF32 array of the outputs completed by this input (0, 1, or more). |
Examples:
>>> from doppler.resample import RateConverter
>>> import numpy as np
>>> rc = RateConverter(rate=0.8, compensate=0) # -> Resampler(0.8)
>>> x = (np.arange(10, dtype=np.float32) + 1).astype(np.complex64)
>>> # a decimator emits 0 between strobes, 1 on a strobe:
>>> [rc.execute_ctrl_push(complex(v), 0.0).shape[0] for v in x]
[0, 1, 1, 1, 1, 0, 1, 1, 1, 1]
reset
¶
Zero all sub-stage filter memories. Rate, stage count, and stage types are preserved. Processing from a reset state produces the same output as a freshly created converter fed the same input. Use between signal bursts to suppress transient artefacts from prior filter memory.
Examples:
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 MatchedRateConverter 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 MatchedRateConverter 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 MatchedRateConverter 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 MatchedRateConverter be used in a with statement so its C
resources are released deterministically on exit rather than at
collection time.
Returns:
| Type | Description |
|---|---|
MatchedRateConverter
|
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 MatchedRateConverter.
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. |
...
|
ciccompmf
¶
Design a CIC passband-droop compensator FIR filter. Implements the closed-form Bernoulli-series maximally-flat-error method from Molnar & Vucic (IEEE TCAS-II 58(12):926-930, 2011, DOI 10.1109/TCSII.2011.2172522). The compensator runs at the decimated (output) rate and should be applied after the CIC stage. DC gain is exactly 1.0. Odd M gives symmetric linear-phase taps; even M gives half-sample-shifted linear-phase taps.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
N
|
int
|
CIC filter order (number of integrator/comb stages, >= 1). |
required |
R
|
int
|
CIC decimation factor (>= 2). |
required |
M
|
int
|
Number of compensator taps in |
required |
Returns:
| Type | Description |
|---|---|
NDArray[float64]
|
Output. |
Examples:
kaiser_beta
¶
Compute the Kaiser window beta parameter from stopband attenuation.
Uses the standard Kaiser-Hamming piecewise formulae:
- atten > 50 dB: beta = 0.1102 * (atten - 8.7)
- 21 <= atten <= 50 dB: beta = 0.5842 * (atten - 21)^0.4 + 0.07886 * (atten - 21)
- atten < 21 dB: beta = 0.0 (rectangular window)
Pass the result to np.kaiser(N, beta) or to
Resampler_create_custom via the bank builder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
atten
|
float
|
Desired stopband attenuation in dB (positive number). |
required |
Returns:
| Type | Description |
|---|---|
float
|
Kaiser beta parameter (>= 0.0). |
Examples:
kaiser_num_taps
¶
Estimate the taps-per-phase count for a polyphase Kaiser FIR bank.
Applies the Kaiser length formula to the per-phase normalised prototype
(pb / num_phases, sb / num_phases), rounds up to the next odd symmetric
length, then divides by num_phases to give taps per branch. The result
is the num_taps argument for Resampler_create_custom and the
row count for the bank builder.
The approximation is::
proto_len = 1 + (atten - 8) / (2.285 * 2*pi * delta_f_per_phase)
num_taps = ceil(proto_len / num_phases) + 1
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
num_phases
|
int
|
Number of polyphase branches (power of two, e.g. 4096). |
required |
atten
|
float
|
Desired stopband attenuation in dB. |
required |
pb
|
float
|
Normalised passband edge (0 < pb < sb < 1). |
required |
sb
|
float
|
Normalised stopband edge. |
required |
Returns:
| Type | Description |
|---|---|
int
|
Taps per polyphase branch (>= 1). |
Examples:
rate_convert
¶
Convert samples to a new sample rate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
(array_like, complex64)
|
Input samples. |
required |
rate
|
float
|
Output-to-input sample rate ratio. |
required |
rc
|
RateConverter
|
Existing converter to reuse; a new one is created if None. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
out |
(ndarray, complex64)
|
Converted samples. |
rc |
RateConverter
|
The converter used (pass back in to maintain state across calls). |
Examples:
options: members: - RateConverter - rate_convert - Resampler - HalfbandDecimator - HalfbandDecimatorQ15 - HalfbandDecimatorDp - HalfbandDecimatorR2C - CIC - ciccompmf - Farrow - kaiser_beta - kaiser_num_taps
Related pages¶
Gallery — CIC Decimation Filter, Doppler Channel — Clock Doppler as a Propagation Impairment, Farrow Interpolator, RateConverter — Automatic Cascade Selection Design — Quantization Design, API taxonomy: the DSP building-block hierarchy and its naming axis, Asynchronous symbol/code despreading, Corr2D: decoupled (interpolated) inverse length, Measurement Suite — single-tone ADC / spectral metrics, MPSK Receiver