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:
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:
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. |
Examples:
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. |
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:
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:
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:
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:
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.
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:
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:
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 - HalfbandDecimatorDp - HalfbandDecimatorR2C - CIC - ciccompmf