Skip to content

Python Resample API

Three resampler implementations backed by the native C library — all accept and return complex64 NumPy arrays with state preserved across calls.

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

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)
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, …).

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)

# Custom Kaiser spec (tighter transition band)
r4 = Resampler(0.5, rejection=80.0, passband=0.35, stopband=0.45)

Rate-controlled resampling (FM/Doppler correction)

Per-sample rate deviation via execute_ctrl:

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
import numpy as np

decim = HalfbandDecimator()          # built-in Kaiser prototype
x = np.random.randn(4096).astype(np.complex64)
y = decim.execute(x)                 # len(y) = 2048

Phase-continuous across blocks:

for block in iq_stream:              # 4096-sample CF32 arrays
    y = decim.execute(block)
    next_stage(y)


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.


resample

resample — Polyphase resampler and halfband decimator types.

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

rate constructor parameter.

0.0

Examples:

Create with defaults:

>>> from doppler.resample import Resampler
>>> obj = Resampler(rate=0.0)

rate property writable

rate: float

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

num_phases: int

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

num_taps: int

Taps per polyphase branch. Total prototype filter length is num_phases * num_taps - 1. The built-in bank uses 19 taps per branch.

execute

execute(x: NDArray[complex64]) -> NDArray[np.complex64]

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.

Examples:

>>> from doppler.resample import Resampler
>>> import numpy as np
>>> r = Resampler(rate=2.0)
>>> y = r.execute(np.zeros(128, dtype=np.complex64))
>>> y.shape, y.dtype
((256,), dtype('complex64'))

execute_ctrl

execute_ctrl(x: NDArray[complex64], ctrl: NDArray[complex64]) -> NDArray[np.complex64]

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.

Examples:

>>> from doppler.resample import Resampler
>>> import numpy as np
>>> r = Resampler(rate=1.0)
>>> x = np.zeros(64, dtype=np.complex64)
>>> ctrl = np.zeros(64, dtype=np.complex64)
>>> y = r.execute_ctrl(x, ctrl)
>>> y.shape, y.dtype
((64,), dtype('complex64'))

reset

reset() -> None

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:

>>> from doppler.resample import Resampler
>>> import numpy as np
>>> r = Resampler(rate=2.0)
>>> _ = r.execute(np.ones(64, dtype=np.complex64))
>>> r.reset()
>>> r.rate
2.0

destroy

destroy() -> None

Release C resources immediately.

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.

Parameters:

Name Type Description Default
R int

R constructor parameter.

16

Examples:

Create with defaults:

>>> from doppler.resample import CIC
>>> obj = CIC(R=16)

R property

R: int

R.

shift property

shift: int

Shift.

reset

reset() -> None

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:

>>> from doppler.resample import CIC
>>> cic = CIC(R=16)
>>> cic.reset()
>>> cic.R
16

reconfigure

reconfigure(R: int) -> None

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:

>>> from doppler.resample import CIC
>>> cic = CIC(R=4)
>>> cic.reconfigure(8)
>>> cic.R, cic.shift
(8, 12)

decimate

decimate(x: complex) -> NDArray[np.complex64]

Decimate.

destroy

destroy() -> None

Release C resources immediately.

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

rate constructor parameter.

1.0
compensate int

compensate constructor parameter.

0

Examples:

Create with defaults:

>>> from doppler.resample import RateConverter
>>> obj = RateConverter(rate=1.0, compensate=0)

rate property writable

rate: float

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.

execute

execute(x: NDArray[complex64]) -> NDArray[np.complex64]

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:

>>> from doppler.resample import RateConverter
>>> import numpy as np
>>> rc = RateConverter(rate=0.5, compensate=0)
>>> y = rc.execute(np.zeros(1024, dtype=np.complex64))
>>> y.shape, y.dtype
((512,), dtype('complex64'))

reset

reset() -> None

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:

>>> from doppler.resample import RateConverter
>>> rc = RateConverter(rate=0.5, compensate=0)
>>> rc.reset()
>>> rc.rate
0.5

destroy

destroy() -> None

Release C resources immediately.

ciccompmf

ciccompmf(N: int, R: int, M: int) -> NDArray[np.float64]

Ciccompmf.

kaiser_beta

kaiser_beta(atten: float) -> float

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:

>>> from doppler.resample import kaiser_beta
>>> round(kaiser_beta(60.0), 4)
5.6533
>>> kaiser_beta(20.0)
0.0

kaiser_num_taps

kaiser_num_taps(num_phases: int, atten: float, pb: float, sb: float) -> int

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:

>>> from doppler.resample import kaiser_num_taps
>>> kaiser_num_taps(4096, 60.0, 0.4, 0.6)
19

rate_convert

rate_convert(x, rate, rc=None)

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:

>>> import numpy as np
>>> from doppler.resample import rate_convert
>>> x = np.ones(256, dtype=np.complex64)
>>> y, rc = rate_convert(x, 0.5)
>>> len(y) == 128
True

options: members: - RateConverter - rate_convert - Resampler - HalfbandDecimator - HalfbandDecimatorDp - HalfbandDecimatorR2C - CIC - ciccompmf