RateConverter¶
RateConverter is a single-class interface to all of doppler's decimation and
interpolation primitives. It inspects the requested rate ratio at construction
time and builds the cheapest cascade — halfband, CIC, or polyphase Resampler —
without any configuration from the caller.
Source: examples/python/rate_converter_demo.py
Stage selection¶
from doppler.resample import RateConverter
rates = [2.0, 0.5, 0.25, 0.125, 0.1, 1/3]
for rate in rates:
rc = RateConverter(rate)
print(f"{rate:.4f} {' → '.join(rc.stages)}")
2.0000 Resampler(2)
0.5000 HalfbandDecimator
0.2500 HalfbandDecimator → HalfbandDecimator
0.1250 CIC(8)
0.1000 CIC(8) → Resampler(0.8)
0.3333 Resampler(0.333333)
The halfband and CIC paths are the fastest for power-of-two decimation ratios because they require far fewer multiplications per output sample than a general polyphase filter. A non-integer ratio like 0.1 (= 1/10) is handled by rounding to the nearest power-of-two CIC (R=8) and then correcting the residual 0.8× with the polyphase Resampler.
Frequency preservation¶
A tone at input normalised frequency fn appears at fn/rate in the output — the RateConverter scales the frequency axis exactly as expected:
import numpy as np
from doppler.resample import RateConverter
fn_in = 0.04
n_in = 4096
x = np.exp(2j * np.pi * fn_in * np.arange(n_in)).astype(np.complex64)
for rate in [0.5, 0.25, 0.125, 1/3]:
rc = RateConverter(rate)
y = rc.execute(x)[len(rc.execute(x)) // 20:] # drop transient
peak = np.fft.fftfreq(len(y))[np.argmax(np.abs(np.fft.fft(y)))]
print(f"rate={rate:.4f} expected fn_out={fn_in/rate:.4f} "
f"measured={peak:.4f}")
rate=0.5000 expected fn_out=0.0800 measured=0.0802
rate=0.2500 expected fn_out=0.1600 measured=0.1603
rate=0.1250 expected fn_out=0.3200 measured=0.3203
rate=0.3333 expected fn_out=0.1200 measured=0.1203
(The small offset is FFT-bin resolution, not resampler error.)
Rate change at runtime¶
Assign to .rate to rebuild the cascade in place. Filter state is reset and
the output buffer is reallocated if the new rate requires a larger buffer:
import numpy as np
from doppler.resample import RateConverter
x = np.random.randn(1024).astype(np.complex64)
rc = RateConverter(0.5)
y1 = rc.execute(x); print(len(y1)) # 512
rc.rate = 0.25
y2 = rc.execute(x); print(len(y2)) # 256
rc.rate = 2.0
y3 = rc.execute(x); print(len(y3)) # 2048
Streaming — phase-continuous across blocks¶
execute() carries filter state across calls, so a stream split at any block
boundary is byte-identical to one large call.
The result is a zero-copy view — copy it to keep it
execute() returns a zero-copy view into the converter's internal
output buffer. The view stays valid only until you next touch the converter
— .copy() it first if you need to keep it. Two things invalidate it:
- The next
execute()reuses the buffer in place — so holding/ concatenating results without copying gives you the latest block's data in every earlier array. reset(), assigning.rate, or a block larger than any seen so far reallocates the buffer — any previously-returned array then points at freed memory (don't read it).
The common streaming loop (fixed block size, consume each block before the next call) needs no copy and never reallocates — this only matters if you retain results across those operations.
import numpy as np
from doppler.resample import RateConverter
x = np.random.randn(2048).astype(np.complex64)
y_full = RateConverter(0.5).execute(x).copy()
rc = RateConverter(0.5)
y_split = np.concatenate([
rc.execute(x[:1024]).copy(), # copy: the next execute() reuses the buffer
rc.execute(x[1024:]).copy(),
])
assert np.array_equal(y_full, y_split) # byte-identical ✓
CIC droop compensation¶
The compensate=1 flag appends a passband-droop compensating FIR after any
CIC stage, designed with ciccompmf(N=4, R=R, M=7):
from doppler.resample import RateConverter
rc_plain = RateConverter(0.125)
rc_comp = RateConverter(0.125, compensate=1)
print(rc_plain.stages) # ['CIC(8)']
print(rc_comp.stages) # ['CIC(8)+FIR']
The compensating FIR corrects the |sin(x)/x|⁴ roll-off over the CIC
passband at negligible extra cost (7 taps running at the decimated rate).
Functional interface¶
rate_convert() is a one-liner wrapper that creates a RateConverter on the
first call and returns it so state can be maintained across calls:
from doppler.resample import rate_convert
import numpy as np
x = np.random.randn(4096).astype(np.complex64)
y1, rc = rate_convert(x, 0.5) # creates RateConverter(0.5)
y2, rc = rate_convert(x, 0.5, rc=rc) # reuses it — state preserved
Run the full demo:
See API reference for the complete method and property listing.