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. 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/64 plans a bare CIC(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 compensate defaults 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

rate: float

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

num_taps: int

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

execute(
    x: NDArray[complex64],
    out: NDArray[complex64] | None = None,
) -> NDArray[np.complex64]

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:

>>> 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)
>>> y = hb.execute(np.zeros(100, dtype=np.complex64))
>>> y.shape, y.dtype
((50,), dtype('complex64'))

execute_max_out

execute_max_out() -> int

Always returns HBDECIM_MAX_OUT.

Returns:

Type Description
int

Output.

reset

reset() -> None

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:

>>> 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.execute(np.ones(64, dtype=np.complex64))
>>> hb.reset()
>>> hb.num_taps
5

state_bytes

state_bytes() -> int

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

get_state() -> bytes

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, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

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 get_state() blob from this type, exactly state_bytes() long.

required

destroy

destroy() -> None

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__() -> HalfbandDecimator

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

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],
    out: NDArray[complex64] | None = None,
) -> 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, capped at max_out.

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_max_out

execute_max_out() -> int

Always returns RESAMPLER_MAX_OUT.

Returns:

Type Description
int

Output.

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, capped at max_out.

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

execute_ctrl_max_out

execute_ctrl_max_out() -> int

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 out= buffer.

Examples:

>>> from doppler.resample import Resampler
>>> Resampler(rate=2.0).execute_ctrl_max_out()
65536

state_bytes

state_bytes() -> int

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

get_state() -> bytes

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, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

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 get_state() blob from this type, exactly state_bytes() long.

required

destroy

destroy() -> None

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__() -> Resampler

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 [2, 4096]. Returns NULL for R=0, non-power-of-two, or R > 4096.

16

Examples:

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

R property

R: int

R.

shift property

shift: int

Shift.

clipped property

clipped: bool

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

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: NDArray[complex64],
    out: NDArray[complex64] | None = None,
) -> NDArray[np.complex64]

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:

>>> from doppler.resample import CIC
>>> import numpy as np
>>> cic = CIC(R=16)
>>> for _ in range(4):
...     _ = cic.decimate(np.zeros(16, dtype=np.complex64))
>>> y = cic.decimate(np.zeros(16, dtype=np.complex64))
>>> y.tolist(), y.dtype
([0j], dtype('complex64'))

decimate_max_out

decimate_max_out() -> int

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

state_bytes() -> int

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

get_state() -> bytes

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, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

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 get_state() blob from this type, exactly state_bytes() long.

required

destroy

destroy() -> None

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__() -> CIC

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

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.

clipped property

clipped: bool

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

narrow_pulse: bool

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

stages: list[str]

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

bank_shape: list[int]

[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

execute(
    x: NDArray[complex64],
    out: NDArray[complex64] | None = None,
) -> 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'))

execute_max_out

execute_max_out() -> int

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

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

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

execute_ctrl_push(
    x: complex, ctrl: float
) -> NDArray[np.complex64]

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

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

state_bytes

state_bytes() -> int

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

get_state() -> bytes

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, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

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 get_state() blob from this type, exactly state_bytes() long.

required

destroy

destroy() -> None

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__() -> RateConverter

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:

>>> from doppler.resample import Farrow
>>> obj = Farrow(order="cubic")

group_delay property

group_delay: int

Group delay.

delay

delay(
    x: NDArray[complex64], mu: float
) -> NDArray[np.complex64]

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 [0,1) into the interpolation interval (values outside extrapolate).

required

Returns:

Type Description
NDArray[complex64]

CF32 output array, same length as x, each sample delayed by group_delay - mu.

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

reset() -> None

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

delay_max_out() -> int

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 out= buffer from len(x).

Examples:

>>> from doppler.resample import Farrow
>>> Farrow().delay_max_out()
0

state_bytes

state_bytes() -> int

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

get_state() -> bytes

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, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

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 get_state() blob from this type, exactly state_bytes() long.

required

destroy

destroy() -> None

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__() -> Farrow

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 (h[k] == h[num_taps-1-k]).

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

num_taps: int

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

rate: float

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

execute(
    x: NDArray[int16], out: NDArray[int16] | None = None
) -> NDArray[np.int16]

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

execute_max_out() -> int

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

reset() -> None

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

state_bytes() -> int

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

get_state() -> bytes

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, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

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 get_state() blob from this type, exactly state_bytes() long.

required

destroy

destroy() -> None

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__() -> HalfbandDecimatorQ15

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

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.

clipped property

clipped: bool

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

narrow_pulse: bool

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

stages: list[str]

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

bank_shape: list[int]

[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

execute(
    x: NDArray[complex64],
    out: NDArray[complex64] | None = None,
) -> 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'))

execute_max_out

execute_max_out() -> int

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

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

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

execute_ctrl_push(
    x: complex, ctrl: float
) -> NDArray[np.complex64]

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

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

state_bytes

state_bytes() -> int

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

get_state() -> bytes

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, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

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 get_state() blob from this type, exactly state_bytes() long.

required

destroy

destroy() -> None

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__() -> MatchedRateConverter

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

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

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 [1, 19] (odd or even).

required

Returns:

Type Description
NDArray[float64]

Output.

Examples:

>>> from doppler.resample import ciccompmf
>>> import numpy as np
>>> h = ciccompmf(4, 16, 5)
>>> h.shape, h.dtype
((5,), dtype('float64'))
>>> [round(float(v), 4) for v in h]
[0.029, -0.282, 1.5061, -0.282, 0.029]

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 - HalfbandDecimatorQ15 - HalfbandDecimatorDp - HalfbandDecimatorR2C - CIC - ciccompmf - Farrow - kaiser_beta - kaiser_num_taps

GalleryCIC Decimation Filter, Doppler Channel — Clock Doppler as a Propagation Impairment, Farrow Interpolator, RateConverter — Automatic Cascade Selection DesignQuantization 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